diff --git a/src/atproto/identity.zig b/src/atproto/identity.zig index f0ca81c..6e8fa59 100644 --- a/src/atproto/identity.zig +++ b/src/atproto/identity.zig @@ -3,6 +3,7 @@ const auth = @import("../auth/tokens.zig"); const config = @import("../core/config.zig"); const mail = @import("../core/mail.zig"); const http_api = @import("../http/api.zig"); +const email_tokens = @import("../internal/email_tokens.zig"); const scopes = @import("../internal/scopes.zig"); const plc = @import("plc.zig"); const store = @import("../storage/store.zig"); @@ -53,7 +54,7 @@ pub fn requestPlcOperationSignature(request: *http_api.Request) !void { return http_api.xrpcError(request, .not_found, "AccountNotFound", "Account not found"); }; var code_buf: [11]u8 = undefined; - const code = makeCode(&code_buf); + const code = email_tokens.makeCode(&code_buf); try store.setAuthCode(account.did, code, store.nowMs() + (10 * 60 * 1000)); try mail.sendCode(store.currentIo(), allocator, info.email, account.handle, "PLC operation", "PLC operation", code); return http_api.json(request, .ok, "{}"); @@ -421,15 +422,3 @@ fn jsonArrayFirstStringEquals(value: ?std.json.Value, expected: []const u8) bool else => false, }; } - -fn makeCode(out: *[11]u8) []const u8 { - const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; - var random: [10]u8 = undefined; - store.randomBytes(&random); - for (random, 0..) |byte, idx| { - const write_idx = if (idx < 5) idx else idx + 1; - out[write_idx] = alphabet[byte % alphabet.len]; - } - out[5] = '-'; - return out[0..]; -} diff --git a/src/atproto/server.zig b/src/atproto/server.zig index 5bba324..2d781c3 100644 --- a/src/atproto/server.zig +++ b/src/atproto/server.zig @@ -6,6 +6,7 @@ const mail = @import("../core/mail.zig"); const plc = @import("plc.zig"); const sync = @import("sync.zig"); const http_api = @import("../http/api.zig"); +const email_tokens = @import("../internal/email_tokens.zig"); const scopes = @import("../internal/scopes.zig"); const store = @import("../storage/store.zig"); const zat = @import("zat"); @@ -1064,7 +1065,7 @@ pub fn requestEmailConfirmation(request: *http_api.Request) !void { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "email already confirmed"); } var code_buf: [11]u8 = undefined; - const code = makeCode(&code_buf); + const code = email_tokens.makeCode(&code_buf); try store.setAuthCode(account.did, code, expiresInTenMinutes()); try sendCode(allocator, info.email, account.handle, "Confirm email", "email confirmation", code); return http_api.json(request, .ok, "{}"); @@ -1114,7 +1115,7 @@ pub fn requestEmailUpdate(request: *http_api.Request) !void { return http_api.json(request, .ok, "{\"tokenRequired\":false}"); } var code_buf: [11]u8 = undefined; - const code = makeCode(&code_buf); + const code = email_tokens.makeCode(&code_buf); try store.setAuthCode(account.did, code, expiresInTenMinutes()); try sendCode(allocator, info.email, account.handle, "Update email", "email update", code); return http_api.json(request, .ok, "{\"tokenRequired\":true}"); @@ -1228,18 +1229,6 @@ fn expiresInTenMinutes() i64 { return store.nowMs() + (10 * 60 * 1000); } -fn makeCode(out: *[11]u8) []const u8 { - const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; - var random: [10]u8 = undefined; - store.randomBytes(&random); - for (random, 0..) |byte, idx| { - const write_idx = if (idx < 5) idx else idx + 1; - out[write_idx] = alphabet[byte % alphabet.len]; - } - out[5] = '-'; - return out[0..]; -} - fn generateAppPassword(allocator: std.mem.Allocator) ![]const u8 { const alphabet = "abcdefghijklmnopqrstuvwxyz234567"; var random: [16]u8 = undefined; diff --git a/src/internal/email_tokens.zig b/src/internal/email_tokens.zig new file mode 100644 index 0000000..afe9efa --- /dev/null +++ b/src/internal/email_tokens.zig @@ -0,0 +1,26 @@ +const std = @import("std"); +const store = @import("../storage/store.zig"); + +pub fn makeCode(out: *[11]u8) []const u8 { + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + var random: [10]u8 = undefined; + store.randomBytes(&random); + for (random, 0..) |byte, idx| { + const write_idx = if (idx < 5) idx else idx + 1; + out[write_idx] = alphabet[byte & 31]; + } + out[5] = '-'; + return out[0..]; +} + +test "email codes use social-app compatible base32 shape" { + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + var out: [11]u8 = undefined; + const code = makeCode(&out); + try std.testing.expectEqual(@as(usize, 11), code.len); + try std.testing.expectEqual(@as(u8, '-'), code[5]); + for (code, 0..) |byte, idx| { + if (idx == 5) continue; + try std.testing.expect(std.mem.indexOfScalar(u8, alphabet, byte) != null); + } +} diff --git a/src/root.zig b/src/root.zig index 15fb915..addac9b 100644 --- a/src/root.zig +++ b/src/root.zig @@ -34,6 +34,7 @@ pub const http = struct { pub const internal = struct { pub const api_reference = @import("internal/api_reference/root.zig"); pub const cli = @import("internal/cli.zig"); + pub const email_tokens = @import("internal/email_tokens.zig"); pub const passkeys = @import("internal/passkeys.zig"); pub const permissioned_data = @import("internal/permissioned_data.zig"); pub const scopes = @import("internal/scopes.zig");