diff options
Diffstat (limited to 'src/nkeys.zig')
-rw-r--r-- | src/nkeys.zig | 722 |
1 files changed, 0 insertions, 722 deletions
diff --git a/src/nkeys.zig b/src/nkeys.zig deleted file mode 100644 index 05922bd..0000000 --- a/src/nkeys.zig +++ /dev/null | |||
@@ -1,722 +0,0 @@ | |||
1 | const std = @import("std"); | ||
2 | const ascii = std.ascii; | ||
3 | const base32 = @import("base32.zig"); | ||
4 | const crc16 = @import("crc16.zig"); | ||
5 | const crypto = std.crypto; | ||
6 | const Ed25519 = crypto.sign.Ed25519; | ||
7 | const mem = std.mem; | ||
8 | const testing = std.testing; | ||
9 | |||
10 | pub const InvalidPrefixByteError = error{InvalidPrefixByte}; | ||
11 | pub const InvalidEncodingError = error{InvalidEncoding}; | ||
12 | pub const InvalidPrivateKeyError = error{InvalidPrivateKey}; | ||
13 | pub const InvalidSeedError = error{InvalidSeed}; | ||
14 | pub const InvalidSignatureError = error{InvalidSignature}; | ||
15 | pub const NoNkeySeedFoundError = error{NoNkeySeedFound}; | ||
16 | pub const NoNkeyUserSeedFoundError = error{NoNkeyUserSeedFound}; | ||
17 | pub const DecodeError = InvalidPrefixByteError || base32.DecodeError || crc16.InvalidChecksumError; | ||
18 | pub const SeedDecodeError = DecodeError || InvalidSeedError || crypto.errors.IdentityElementError; | ||
19 | pub const PrivateKeyDecodeError = DecodeError || InvalidPrivateKeyError || crypto.errors.IdentityElementError; | ||
20 | pub const SignError = crypto.errors.IdentityElementError || crypto.errors.WeakPublicKeyError || crypto.errors.KeyMismatchError; | ||
21 | |||
22 | pub const prefix_byte_account = 0; // A | ||
23 | pub const prefix_byte_cluster = 2 << 3; // C | ||
24 | pub const prefix_byte_operator = 14 << 3; // O | ||
25 | pub const prefix_byte_private = 15 << 3; // P | ||
26 | pub const prefix_byte_seed = 18 << 3; // S | ||
27 | pub const prefix_byte_server = 13 << 3; // N | ||
28 | pub const prefix_byte_user = 20 << 3; // U | ||
29 | |||
30 | pub fn prefixByteToLetter(prefix_byte: u8) ?u8 { | ||
31 | return switch (prefix_byte) { | ||
32 | prefix_byte_account => 'A', | ||
33 | prefix_byte_cluster => 'C', | ||
34 | prefix_byte_operator => 'O', | ||
35 | prefix_byte_private => 'P', | ||
36 | prefix_byte_seed => 'S', | ||
37 | prefix_byte_server => 'N', | ||
38 | prefix_byte_user => 'U', | ||
39 | else => null, | ||
40 | }; | ||
41 | } | ||
42 | |||
43 | pub fn prefixByteFromLetter(letter: u8) ?u8 { | ||
44 | return switch (letter) { | ||
45 | 'A' => prefix_byte_account, | ||
46 | 'C' => prefix_byte_cluster, | ||
47 | 'O' => prefix_byte_operator, | ||
48 | 'P' => prefix_byte_private, | ||
49 | 'S' => prefix_byte_seed, | ||
50 | 'N' => prefix_byte_server, | ||
51 | 'U' => prefix_byte_user, | ||
52 | else => null, | ||
53 | }; | ||
54 | } | ||
55 | |||
56 | pub const Role = enum(u8) { | ||
57 | const Self = @This(); | ||
58 | |||
59 | account, | ||
60 | cluster, | ||
61 | operator, | ||
62 | server, | ||
63 | user, | ||
64 | |||
65 | pub fn fromPublicPrefixByte(b: u8) ?Self { | ||
66 | return switch (b) { | ||
67 | prefix_byte_account => .account, | ||
68 | prefix_byte_cluster => .cluster, | ||
69 | prefix_byte_operator => .operator, | ||
70 | prefix_byte_server => .server, | ||
71 | prefix_byte_user => .user, | ||
72 | else => null, | ||
73 | }; | ||
74 | } | ||
75 | |||
76 | pub fn publicPrefixByte(self: Self) u8 { | ||
77 | return switch (self) { | ||
78 | .account => prefix_byte_account, | ||
79 | .cluster => prefix_byte_cluster, | ||
80 | .operator => prefix_byte_operator, | ||
81 | .server => prefix_byte_server, | ||
82 | .user => prefix_byte_user, | ||
83 | }; | ||
84 | } | ||
85 | |||
86 | pub fn letter(self: Self) u8 { | ||
87 | return prefixByteToLetter(self.publicPrefixByte()) orelse unreachable; | ||
88 | } | ||
89 | }; | ||
90 | |||
91 | // One prefix byte, two CRC bytes | ||
92 | const binary_private_size = 1 + Ed25519.secret_length + 2; | ||
93 | // One prefix byte, two CRC bytes | ||
94 | const binary_public_size = 1 + Ed25519.public_length + 2; | ||
95 | // Two prefix bytes, two CRC bytes | ||
96 | const binary_seed_size = 2 + Ed25519.seed_length + 2; | ||
97 | |||
98 | pub const text_private_len = base32.Encoder.calcSize(binary_private_size); | ||
99 | pub const text_public_len = base32.Encoder.calcSize(binary_public_size); | ||
100 | pub const text_seed_len = base32.Encoder.calcSize(binary_seed_size); | ||
101 | |||
102 | pub const text_private = [text_private_len]u8; | ||
103 | pub const text_public = [text_public_len]u8; | ||
104 | pub const text_seed = [text_seed_len]u8; | ||
105 | |||
106 | pub const SeedKeyPair = struct { | ||
107 | const Self = @This(); | ||
108 | |||
109 | role: Role, | ||
110 | kp: Ed25519.KeyPair, | ||
111 | |||
112 | pub fn generate(role: Role) crypto.errors.IdentityElementError!Self { | ||
113 | var raw_seed: [Ed25519.seed_length]u8 = undefined; | ||
114 | crypto.random.bytes(&raw_seed); | ||
115 | defer wipeBytes(&raw_seed); | ||
116 | return Self{ .role = role, .kp = try Ed25519.KeyPair.create(raw_seed) }; | ||
117 | } | ||
118 | |||
119 | pub fn fromTextSeed(text: *const text_seed) SeedDecodeError!Self { | ||
120 | var decoded = try decode(2, Ed25519.seed_length, text); | ||
121 | defer decoded.wipe(); // gets copied | ||
122 | |||
123 | var key_ty_prefix = decoded.prefix[0] & 0b11111000; | ||
124 | var role_prefix = (decoded.prefix[0] << 5) | (decoded.prefix[1] >> 3); | ||
125 | |||
126 | if (key_ty_prefix != prefix_byte_seed) | ||
127 | return error.InvalidSeed; | ||
128 | |||
129 | return Self{ | ||
130 | .role = Role.fromPublicPrefixByte(role_prefix) orelse return error.InvalidPrefixByte, | ||
131 | .kp = try Ed25519.KeyPair.create(decoded.data), | ||
132 | }; | ||
133 | } | ||
134 | |||
135 | pub fn fromRawSeed( | ||
136 | role: Role, | ||
137 | raw_seed: *const [Ed25519.seed_length]u8, | ||
138 | ) crypto.errors.IdentityElementError!Self { | ||
139 | return Self{ .role = role, .kp = try Ed25519.KeyPair.create(raw_seed.*) }; | ||
140 | } | ||
141 | |||
142 | pub fn sign(self: *const Self, msg: []const u8) SignError![Ed25519.signature_length]u8 { | ||
143 | return Ed25519.sign(msg, self.kp, null); | ||
144 | } | ||
145 | |||
146 | pub fn verify(self: *const Self, msg: []const u8, sig: [Ed25519.signature_length]u8) InvalidSignatureError!void { | ||
147 | Ed25519.verify(sig, msg, self.kp.public_key) catch return error.InvalidSignature; | ||
148 | } | ||
149 | |||
150 | pub fn seedText(self: *const Self) text_seed { | ||
151 | const public_prefix = self.role.publicPrefixByte(); | ||
152 | const full_prefix = &[_]u8{ | ||
153 | prefix_byte_seed | (public_prefix >> 5), | ||
154 | (public_prefix & 0b00011111) << 3, | ||
155 | }; | ||
156 | const seed = self.kp.secret_key[0..Ed25519.seed_length]; | ||
157 | return encode(full_prefix.len, seed.len, full_prefix, seed); | ||
158 | } | ||
159 | |||
160 | pub fn privateKeyText(self: *const Self) text_private { | ||
161 | return encode(1, self.kp.secret_key.len, &.{prefix_byte_private}, &self.kp.secret_key); | ||
162 | } | ||
163 | |||
164 | pub fn publicKeyText(self: *const Self) text_public { | ||
165 | return encode(1, self.kp.public_key.len, &.{self.role.publicPrefixByte()}, &self.kp.public_key); | ||
166 | } | ||
167 | |||
168 | pub fn intoPublicKey(self: *const Self) PublicKey { | ||
169 | return PublicKey{ | ||
170 | .role = self.role, | ||
171 | .key = self.kp.public_key, | ||
172 | }; | ||
173 | } | ||
174 | |||
175 | pub fn intoPrivateKey(self: *const Self) PrivateKey { | ||
176 | return PrivateKey{ .kp = self.kp }; | ||
177 | } | ||
178 | |||
179 | pub fn wipe(self: *Self) void { | ||
180 | self.role = .account; | ||
181 | wipeKeyPair(&self.kp); | ||
182 | } | ||
183 | }; | ||
184 | |||
185 | pub const PublicKey = struct { | ||
186 | const Self = @This(); | ||
187 | |||
188 | role: Role, | ||
189 | key: [Ed25519.public_length]u8, | ||
190 | |||
191 | pub fn fromTextPublicKey(text: *const text_public) DecodeError!Self { | ||
192 | var decoded = try decode(1, Ed25519.public_length, text); | ||
193 | defer decoded.wipe(); // gets copied | ||
194 | return PublicKey{ | ||
195 | .role = Role.fromPublicPrefixByte(decoded.prefix[0]) orelse return error.InvalidPrefixByte, | ||
196 | .key = decoded.data, | ||
197 | }; | ||
198 | } | ||
199 | |||
200 | pub fn fromRawPublicKey(role: Role, raw_key: *const [Ed25519.public_length]u8) Self { | ||
201 | return Self{ .role = role, .key = raw_key.* }; | ||
202 | } | ||
203 | |||
204 | pub fn publicKeyText(self: *const Self) text_public { | ||
205 | return encode(1, self.key.len, &.{self.role.publicPrefixByte()}, &self.key); | ||
206 | } | ||
207 | |||
208 | pub fn verify(self: *const Self, msg: []const u8, sig: [Ed25519.signature_length]u8) InvalidSignatureError!void { | ||
209 | Ed25519.verify(sig, msg, self.key) catch return error.InvalidSignature; | ||
210 | } | ||
211 | |||
212 | pub fn wipe(self: *Self) void { | ||
213 | self.role = .account; | ||
214 | wipeBytes(&self.key); | ||
215 | } | ||
216 | }; | ||
217 | |||
218 | pub const PrivateKey = struct { | ||
219 | const Self = @This(); | ||
220 | |||
221 | kp: Ed25519.KeyPair, | ||
222 | |||
223 | pub fn fromTextPrivateKey(text: *const text_private) PrivateKeyDecodeError!Self { | ||
224 | var decoded = try decode(1, Ed25519.secret_length, text); | ||
225 | defer decoded.wipe(); // gets copied | ||
226 | if (decoded.prefix[0] != prefix_byte_private) | ||
227 | return error.InvalidPrivateKey; | ||
228 | return PrivateKey{ .kp = Ed25519.KeyPair.fromSecretKey(decoded.data) }; | ||
229 | } | ||
230 | |||
231 | pub fn fromRawPrivateKey(raw_key: *const [Ed25519.secret_length]u8) Self { | ||
232 | return Self{ .kp = Ed25519.KeyPair.fromSecretKey(raw_key.*) }; | ||
233 | } | ||
234 | |||
235 | pub fn intoSeedKeyPair(self: *const Self, role: Role) SeedKeyPair { | ||
236 | return SeedKeyPair{ | ||
237 | .role = role, | ||
238 | .kp = self.kp, | ||
239 | }; | ||
240 | } | ||
241 | |||
242 | pub fn intoPublicKey(self: *const Self, role: Role) PublicKey { | ||
243 | return PublicKey{ | ||
244 | .role = role, | ||
245 | .key = self.kp.public_key, | ||
246 | }; | ||
247 | } | ||
248 | |||
249 | pub fn privateKeyText(self: *const Self) text_private { | ||
250 | return encode(1, self.kp.secret_key.len, &.{prefix_byte_private}, &self.kp.secret_key); | ||
251 | } | ||
252 | |||
253 | pub fn sign(self: *const Self, msg: []const u8) SignError![Ed25519.signature_length]u8 { | ||
254 | return Ed25519.sign(msg, self.kp, null); | ||
255 | } | ||
256 | |||
257 | pub fn verify(self: *const Self, msg: []const u8, sig: [Ed25519.signature_length]u8) InvalidSignatureError!void { | ||
258 | Ed25519.verify(sig, msg, self.kp.public_key) catch return error.InvalidSignature; | ||
259 | } | ||
260 | |||
261 | pub fn wipe(self: *Self) void { | ||
262 | wipeKeyPair(&self.kp); | ||
263 | } | ||
264 | }; | ||
265 | |||
266 | fn encoded_key(comptime prefix_len: usize, comptime data_len: usize) type { | ||
267 | return [base32.Encoder.calcSize(prefix_len + data_len + 2)]u8; | ||
268 | } | ||
269 | |||
270 | fn encode( | ||
271 | comptime prefix_len: usize, | ||
272 | comptime data_len: usize, | ||
273 | prefix: *const [prefix_len]u8, | ||
274 | data: *const [data_len]u8, | ||
275 | ) encoded_key(prefix_len, data_len) { | ||
276 | var buf: [prefix_len + data_len + 2]u8 = undefined; | ||
277 | defer wipeBytes(&buf); | ||
278 | |||
279 | mem.copy(u8, &buf, prefix[0..]); | ||
280 | mem.copy(u8, buf[prefix_len..], data[0..]); | ||
281 | var off = prefix_len + data_len; | ||
282 | var checksum = crc16.make(buf[0..off]); | ||
283 | mem.writeIntLittle(u16, buf[buf.len - 2 .. buf.len], checksum); | ||
284 | |||
285 | var text: encoded_key(prefix_len, data_len) = undefined; | ||
286 | std.debug.assert(base32.Encoder.encode(&text, &buf).len == text.len); | ||
287 | |||
288 | return text; | ||
289 | } | ||
290 | |||
291 | fn DecodedNkey(comptime prefix_len: usize, comptime data_len: usize) type { | ||
292 | return struct { | ||
293 | const Self = @This(); | ||
294 | |||
295 | prefix: [prefix_len]u8, | ||
296 | data: [data_len]u8, | ||
297 | |||
298 | pub fn wipe(self: *Self) void { | ||
299 | self.prefix[0] = Role.account.publicPrefixByte(); | ||
300 | wipeBytes(&self.data); | ||
301 | } | ||
302 | }; | ||
303 | } | ||
304 | |||
305 | fn decode( | ||
306 | comptime prefix_len: usize, | ||
307 | comptime data_len: usize, | ||
308 | text: *const [base32.Encoder.calcSize(prefix_len + data_len + 2)]u8, | ||
309 | ) (base32.DecodeError || crc16.InvalidChecksumError)!DecodedNkey(prefix_len, data_len) { | ||
310 | var raw: [prefix_len + data_len + 2]u8 = undefined; | ||
311 | defer wipeBytes(&raw); | ||
312 | std.debug.assert((try base32.Decoder.decode(&raw, text[0..])).len == raw.len); | ||
313 | |||
314 | var checksum = mem.readIntLittle(u16, raw[raw.len - 2 .. raw.len]); | ||
315 | try crc16.validate(raw[0 .. raw.len - 2], checksum); | ||
316 | |||
317 | return DecodedNkey(prefix_len, data_len){ | ||
318 | .prefix = raw[0..prefix_len].*, | ||
319 | .data = raw[prefix_len .. raw.len - 2].*, | ||
320 | }; | ||
321 | } | ||
322 | |||
323 | pub fn isValidEncoding(text: []const u8) bool { | ||
324 | if (text.len < 4) return false; | ||
325 | var made_crc: u16 = 0; | ||
326 | var dec = base32.Decoder.init(text); | ||
327 | var crc_buf: [2]u8 = undefined; | ||
328 | var crc_buf_len: u8 = 0; | ||
329 | var expect_len: usize = base32.Decoder.calcSize(text.len); | ||
330 | var wrote_n_total: usize = 0; | ||
331 | while (dec.next() catch return false) |b| { | ||
332 | wrote_n_total += 1; | ||
333 | if (crc_buf_len == 2) made_crc = crc16.update(made_crc, &.{crc_buf[0]}); | ||
334 | crc_buf[0] = crc_buf[1]; | ||
335 | crc_buf[1] = b; | ||
336 | if (crc_buf_len != 2) crc_buf_len += 1; | ||
337 | } | ||
338 | std.debug.assert(wrote_n_total == expect_len); | ||
339 | if (crc_buf_len != 2) unreachable; | ||
340 | var got_crc = mem.readIntLittle(u16, &crc_buf); | ||
341 | return made_crc == got_crc; | ||
342 | } | ||
343 | |||
344 | pub fn isValidSeed(text: []const u8, with_role: ?Role) bool { | ||
345 | if (text.len < text_seed_len) return false; | ||
346 | var res = SeedKeyPair.fromTextSeed(text[0..text_seed_len]) catch return false; | ||
347 | defer res.wipe(); | ||
348 | return if (with_role) |role| res.role == role else true; | ||
349 | } | ||
350 | |||
351 | pub fn isValidPublicKey(text: []const u8, with_role: ?Role) bool { | ||
352 | if (text.len < text_public_len) return false; | ||
353 | var res = PublicKey.fromTextPublicKey(text[0..text_public_len]) catch return false; | ||
354 | defer res.wipe(); | ||
355 | return if (with_role) |role| res.role == role else true; | ||
356 | } | ||
357 | |||
358 | pub fn isValidPrivateKey(text: []const u8) bool { | ||
359 | if (text.len < text_private_len) return false; | ||
360 | var res = PrivateKey.fromTextPrivateKey(text[0..text_private_len]) catch return false; | ||
361 | res.wipe(); | ||
362 | return true; | ||
363 | } | ||
364 | |||
365 | // `line` must not contain CR or LF characters. | ||
366 | pub fn isKeySectionBarrier(line: []const u8, opening: bool) bool { | ||
367 | if (line.len < 6) return false; | ||
368 | const start = mem.indexOf(u8, line, "---") orelse return false; | ||
369 | if (!opening and start != 0) return false; | ||
370 | if (line.len - start < 6) return false; | ||
371 | return mem.endsWith(u8, line, "---"); | ||
372 | } | ||
373 | |||
374 | const allowed_creds_section_chars_table: [256]bool = allowed: { | ||
375 | var table = [_]bool{false} ** 256; | ||
376 | const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.="; | ||
377 | for (chars) |char| table[char] = true; | ||
378 | break :allowed table; | ||
379 | }; | ||
380 | |||
381 | pub fn areKeySectionContentsValid(contents: []const u8) bool { | ||
382 | for (contents) |c| if (!allowed_creds_section_chars_table[c]) return false; | ||
383 | return true; | ||
384 | } | ||
385 | |||
386 | pub fn findKeySection(text: []const u8, line_it: *std.mem.SplitIterator) ?[]const u8 { | ||
387 | while (true) { | ||
388 | const opening_line = line_it.next() orelse return null; | ||
389 | if (!isKeySectionBarrier(opening_line, true)) continue; | ||
390 | |||
391 | const contents_line = line_it.next() orelse return null; | ||
392 | if (!areKeySectionContentsValid(contents_line)) continue; | ||
393 | |||
394 | const closing_line = line_it.next() orelse return null; | ||
395 | if (!isKeySectionBarrier(closing_line, false)) continue; | ||
396 | |||
397 | return contents_line; | ||
398 | } | ||
399 | } | ||
400 | |||
401 | pub fn parseDecoratedJwt(contents: []const u8) []const u8 { | ||
402 | var line_it = mem.split(contents, "\n"); | ||
403 | return findKeySection(contents, &line_it) orelse return contents; | ||
404 | } | ||
405 | |||
406 | pub fn parseDecoratedNkey(contents: []const u8) NoNkeySeedFoundError!SeedKeyPair { | ||
407 | var line_it = mem.split(contents, "\n"); | ||
408 | var current_off: usize = 0; | ||
409 | var seed: ?[]const u8 = null; | ||
410 | if (findKeySection(contents, &line_it) != null) | ||
411 | seed = findKeySection(contents, &line_it); | ||
412 | if (seed == null) | ||
413 | seed = findNkey(contents) orelse return error.NoNkeySeedFound; | ||
414 | if (!isValidCredsNkey(seed.?)) | ||
415 | return error.NoNkeySeedFound; | ||
416 | return SeedKeyPair.fromTextSeed(seed.?[0..text_seed_len]) catch return error.NoNkeySeedFound; | ||
417 | } | ||
418 | |||
419 | pub fn parseDecoratedUserNkey(contents: []const u8) (NoNkeySeedFoundError || NoNkeyUserSeedFoundError)!SeedKeyPair { | ||
420 | var key = try parseDecoratedNkey(contents); | ||
421 | if (!mem.startsWith(u8, &key.seedText(), "SU")) return error.NoNkeyUserSeedFound; | ||
422 | defer key.wipe(); | ||
423 | return key; | ||
424 | } | ||
425 | |||
426 | fn isValidCredsNkey(text: []const u8) bool { | ||
427 | const valid_prefix = | ||
428 | mem.startsWith(u8, text, "SO") or | ||
429 | mem.startsWith(u8, text, "SA") or | ||
430 | mem.startsWith(u8, text, "SU"); | ||
431 | const valid_len = text.len >= text_seed_len; | ||
432 | return valid_prefix and valid_len; | ||
433 | } | ||
434 | |||
435 | fn findNkey(text: []const u8) ?[]const u8 { | ||
436 | var line_it = std.mem.split(text, "\n"); | ||
437 | var current_off: usize = 0; | ||
438 | while (line_it.next()) |line| { | ||
439 | for (line) |c, i| { | ||
440 | if (!ascii.isSpace(c)) { | ||
441 | if (isValidCredsNkey(line[i..])) return line[i..]; | ||
442 | break; | ||
443 | } | ||
444 | } | ||
445 | } | ||
446 | return null; | ||
447 | } | ||
448 | |||
449 | fn wipeKeyPair(kp: *Ed25519.KeyPair) void { | ||
450 | wipeBytes(&kp.public_key); | ||
451 | wipeBytes(&kp.secret_key); | ||
452 | } | ||
453 | |||
454 | fn wipeBytes(bs: []u8) void { | ||
455 | for (bs) |*b| b.* = 0; | ||
456 | } | ||
457 | |||
458 | test "reference all delcarations" { | ||
459 | testing.refAllDecls(@This()); | ||
460 | testing.refAllDecls(Role); | ||
461 | testing.refAllDecls(SeedKeyPair); | ||
462 | testing.refAllDecls(PublicKey); | ||
463 | testing.refAllDecls(PrivateKey); | ||
464 | } | ||
465 | |||
466 | test "key conversions" { | ||
467 | var key_pair = try SeedKeyPair.generate(.server); | ||
468 | var decoded_seed = try SeedKeyPair.fromTextSeed(&key_pair.seedText()); | ||
469 | try testing.expect(isValidEncoding(&decoded_seed.seedText())); | ||
470 | |||
471 | var pub_key_str_a = key_pair.publicKeyText(); | ||
472 | var priv_key_str_a = key_pair.privateKeyText(); | ||
473 | try testing.expect(pub_key_str_a.len != 0); | ||
474 | try testing.expect(priv_key_str_a.len != 0); | ||
475 | try testing.expect(isValidEncoding(&pub_key_str_a)); | ||
476 | try testing.expect(isValidEncoding(&priv_key_str_a)); | ||
477 | |||
478 | var pub_key = key_pair.intoPublicKey(); | ||
479 | var pub_key_str_b = pub_key.publicKeyText(); | ||
480 | try testing.expectEqualStrings(&pub_key_str_a, &pub_key_str_b); | ||
481 | |||
482 | var priv_key = key_pair.intoPrivateKey(); | ||
483 | var priv_key_str_b = priv_key.privateKeyText(); | ||
484 | try testing.expectEqualStrings(&priv_key_str_a, &priv_key_str_b); | ||
485 | } | ||
486 | |||
487 | test "decode" { | ||
488 | const kp = try SeedKeyPair.generate(.account); | ||
489 | const seed_text = kp.seedText(); | ||
490 | const pub_key_text = kp.publicKeyText(); | ||
491 | const priv_key_text = kp.privateKeyText(); | ||
492 | |||
493 | _ = try SeedKeyPair.fromTextSeed(&seed_text); | ||
494 | _ = try PublicKey.fromTextPublicKey(&pub_key_text); | ||
495 | _ = try PrivateKey.fromTextPrivateKey(&priv_key_text); | ||
496 | |||
497 | try testing.expectError(error.InvalidChecksum, PublicKey.fromTextPublicKey(seed_text[0..text_public_len])); | ||
498 | try testing.expectError(error.InvalidChecksum, SeedKeyPair.fromTextSeed(priv_key_text[0..text_seed_len])); | ||
499 | } | ||
500 | |||
501 | test "seed" { | ||
502 | inline for (@typeInfo(Role).Enum.fields) |field| { | ||
503 | const role = @field(Role, field.name); | ||
504 | const kp = try SeedKeyPair.generate(role); | ||
505 | const decoded = try SeedKeyPair.fromTextSeed(&kp.seedText()); | ||
506 | if (decoded.role != role) { | ||
507 | std.debug.print("expected role {}, found role {}\n", .{ role, decoded.role }); | ||
508 | return error.TestUnexpectedError; | ||
509 | } | ||
510 | } | ||
511 | } | ||
512 | |||
513 | test "public key" { | ||
514 | inline for (@typeInfo(Role).Enum.fields) |field| { | ||
515 | const role = @field(Role, field.name); | ||
516 | const kp = try SeedKeyPair.generate(role); | ||
517 | const decoded_pub_key = try PublicKey.fromTextPublicKey(&kp.publicKeyText()); | ||
518 | if (decoded_pub_key.role != role) { | ||
519 | std.debug.print("expected role {}, found role {}\n", .{ role, decoded_pub_key.role }); | ||
520 | return error.TestUnexpectedError; | ||
521 | } | ||
522 | } | ||
523 | } | ||
524 | |||
525 | test "different key types" { | ||
526 | inline for (@typeInfo(Role).Enum.fields) |field| { | ||
527 | const role = @field(Role, field.name); | ||
528 | |||
529 | const kp = try SeedKeyPair.generate(role); | ||
530 | _ = try SeedKeyPair.fromTextSeed(&kp.seedText()); | ||
531 | |||
532 | const pub_key_str = kp.publicKeyText(); | ||
533 | try testing.expect(pub_key_str[0] == role.letter()); | ||
534 | try testing.expect(isValidPublicKey(&pub_key_str, role)); | ||
535 | |||
536 | const priv_key_str = kp.privateKeyText(); | ||
537 | try testing.expect(priv_key_str[0] == 'P'); | ||
538 | try testing.expect(isValidPrivateKey(&priv_key_str)); | ||
539 | |||
540 | const data = "Hello, world!"; | ||
541 | const sig = try kp.sign(data); | ||
542 | try testing.expect(sig.len == Ed25519.signature_length); | ||
543 | try kp.verify(data, sig); | ||
544 | } | ||
545 | } | ||
546 | |||
547 | test "validation" { | ||
548 | const roles = @typeInfo(Role).Enum.fields; | ||
549 | inline for (roles) |field, i| { | ||
550 | const role = @field(Role, field.name); | ||
551 | const next_role = next: { | ||
552 | const next_field_i = if (i == roles.len - 1) 0 else i + 1; | ||
553 | std.debug.assert(next_field_i != i); | ||
554 | break :next @field(Role, roles[next_field_i].name); | ||
555 | }; | ||
556 | const kp = try SeedKeyPair.generate(role); | ||
557 | |||
558 | const seed_str = kp.seedText(); | ||
559 | const pub_key_str = kp.publicKeyText(); | ||
560 | const priv_key_str = kp.privateKeyText(); | ||
561 | |||
562 | try testing.expect(isValidSeed(&seed_str, role)); | ||
563 | try testing.expect(isValidSeed(&seed_str, null)); | ||
564 | try testing.expect(isValidPublicKey(&pub_key_str, null)); | ||
565 | try testing.expect(isValidPublicKey(&pub_key_str, role)); | ||
566 | try testing.expect(isValidPrivateKey(&priv_key_str)); | ||
567 | |||
568 | try testing.expect(!isValidSeed(&seed_str, next_role)); | ||
569 | try testing.expect(!isValidSeed(&pub_key_str, null)); | ||
570 | try testing.expect(!isValidSeed(&priv_key_str, null)); | ||
571 | try testing.expect(!isValidPublicKey(&pub_key_str, next_role)); | ||
572 | try testing.expect(!isValidPublicKey(&seed_str, null)); | ||
573 | try testing.expect(!isValidPublicKey(&priv_key_str, null)); | ||
574 | try testing.expect(!isValidPrivateKey(&seed_str)); | ||
575 | try testing.expect(!isValidPrivateKey(&pub_key_str)); | ||
576 | } | ||
577 | |||
578 | try testing.expect(!isValidSeed("seed", null)); | ||
579 | try testing.expect(!isValidPublicKey("public key", null)); | ||
580 | try testing.expect(!isValidPrivateKey("private key")); | ||
581 | } | ||
582 | |||
583 | test "from seed" { | ||
584 | const kp = try SeedKeyPair.generate(.account); | ||
585 | const kp_from_raw = try SeedKeyPair.fromRawSeed(kp.role, kp.kp.secret_key[0..Ed25519.seed_length]); | ||
586 | try testing.expect(std.meta.eql(kp, kp_from_raw)); | ||
587 | |||
588 | const data = "Hello, World!"; | ||
589 | const sig = try kp.sign(data); | ||
590 | |||
591 | const seed = kp.seedText(); | ||
592 | try testing.expect(mem.startsWith(u8, &seed, "SA")); | ||
593 | |||
594 | const kp2 = try SeedKeyPair.fromTextSeed(&seed); | ||
595 | try kp2.verify(data, sig); | ||
596 | } | ||
597 | |||
598 | test "from public key" { | ||
599 | const kp = try SeedKeyPair.generate(.user); | ||
600 | |||
601 | const pk_text = kp.publicKeyText(); | ||
602 | const pk_text_clone = kp.publicKeyText(); | ||
603 | try testing.expectEqualStrings(&pk_text, &pk_text_clone); | ||
604 | |||
605 | const pk = try PublicKey.fromTextPublicKey(&pk_text); | ||
606 | const pk_text_clone_2 = pk.publicKeyText(); | ||
607 | try testing.expect(std.meta.eql(pk, kp.intoPublicKey())); | ||
608 | try testing.expect(std.meta.eql(pk, PublicKey.fromRawPublicKey(kp.role, &kp.kp.public_key))); | ||
609 | try testing.expectEqualStrings(&pk_text, &pk_text_clone_2); | ||
610 | |||
611 | const data = "Hello, world!"; | ||
612 | |||
613 | const sig = try kp.sign(data); | ||
614 | try pk.verify(data, sig); | ||
615 | |||
616 | // Create another user to sign and make sure verification fails | ||
617 | const kp2 = try SeedKeyPair.generate(.user); | ||
618 | const sig2 = try kp2.sign(data); | ||
619 | |||
620 | try testing.expectError(error.InvalidSignature, pk.verify(data, sig2)); | ||
621 | } | ||
622 | |||
623 | test "from private key" { | ||
624 | const kp = try SeedKeyPair.generate(.account); | ||
625 | |||
626 | const pk_text = kp.privateKeyText(); | ||
627 | const pk_text_clone = kp.privateKeyText(); | ||
628 | try testing.expectEqualStrings(&pk_text, &pk_text_clone); | ||
629 | |||
630 | const pk = try PrivateKey.fromTextPrivateKey(&pk_text); | ||
631 | const pk_text_clone_2 = pk.privateKeyText(); | ||
632 | try testing.expect(std.meta.eql(pk, kp.intoPrivateKey())); | ||
633 | try testing.expect(std.meta.eql(kp, pk.intoSeedKeyPair(.account))); | ||
634 | try testing.expect(std.meta.eql(pk, PrivateKey.fromRawPrivateKey(&kp.kp.secret_key))); | ||
635 | try testing.expectEqualStrings(&pk_text, &pk_text_clone_2); | ||
636 | |||
637 | const data = "Hello, World!"; | ||
638 | |||
639 | const sig0 = try kp.sign(data); | ||
640 | const sig1 = try pk.sign(data); | ||
641 | try testing.expectEqualSlices(u8, &sig0, &sig1); | ||
642 | try pk.verify(data, sig0); | ||
643 | try kp.verify(data, sig1); | ||
644 | |||
645 | const kp2 = try SeedKeyPair.generate(.account); | ||
646 | const sig2 = try kp2.sign(data); | ||
647 | |||
648 | try testing.expectError(error.InvalidSignature, pk.verify(data, sig2)); | ||
649 | } | ||
650 | |||
651 | test "bad decode" { | ||
652 | const kp = try SeedKeyPair.fromTextSeed("SAAHPQF3GOP4IP5SHKHCNBOHD5TMGSW4QQL6RTZAPEEYOQ2NRBIAKCCLQA"); | ||
653 | |||
654 | var bad_seed = kp.seedText(); | ||
655 | bad_seed[1] = 'S'; | ||
656 | try testing.expectError(error.InvalidChecksum, SeedKeyPair.fromTextSeed(&bad_seed)); | ||
657 | |||
658 | var bad_pub_key = kp.publicKeyText(); | ||
659 | bad_pub_key[bad_pub_key.len - 1] = 'O'; | ||
660 | bad_pub_key[bad_pub_key.len - 2] = 'O'; | ||
661 | try testing.expectError(error.InvalidChecksum, PublicKey.fromTextPublicKey(&bad_pub_key)); | ||
662 | |||
663 | var bad_priv_key = kp.privateKeyText(); | ||
664 | bad_priv_key[bad_priv_key.len - 1] = 'O'; | ||
665 | bad_priv_key[bad_priv_key.len - 2] = 'O'; | ||
666 | try testing.expectError(error.InvalidChecksum, PrivateKey.fromTextPrivateKey(&bad_priv_key)); | ||
667 | } | ||
668 | |||
669 | test "wipe" { | ||
670 | const kp = try SeedKeyPair.generate(.account); | ||
671 | const pub_key = kp.intoPublicKey(); | ||
672 | const priv_key = kp.intoPrivateKey(); | ||
673 | |||
674 | var kp_clone = kp; | ||
675 | kp_clone.wipe(); | ||
676 | try testing.expect(!std.meta.eql(kp_clone.kp, kp.kp)); | ||
677 | |||
678 | var pub_key_clone = pub_key; | ||
679 | pub_key_clone.wipe(); | ||
680 | try testing.expect(!std.meta.eql(pub_key_clone.key, pub_key.key)); | ||
681 | |||
682 | var priv_key_clone = priv_key; | ||
683 | priv_key_clone.wipe(); | ||
684 | try testing.expect(!std.meta.eql(priv_key_clone.kp, priv_key.kp)); | ||
685 | } | ||
686 | |||
687 | test "parse decorated JWT (bad)" { | ||
688 | try testing.expectEqualStrings("foo", parseDecoratedJwt("foo")); | ||
689 | } | ||
690 | |||
691 | test "parse decorated seed (bad)" { | ||
692 | try testing.expectError(error.NoNkeySeedFound, parseDecoratedNkey("foo")); | ||
693 | } | ||
694 | |||
695 | test "parse decorated seed and JWT" { | ||
696 | const creds = | ||
697 | \\-----BEGIN NATS USER JWT----- | ||
698 | \\eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJUWEg1TUxDNTdPTUJUQURYNUJNU0RLWkhSQUtXUFM0TkdHRFFPVlJXRzUyRFdaUlFFVERBIiwiaWF0IjoxNjIxNTgyOTU1LCJpc3MiOiJBQ1ZUQVZMQlFKTklQRjdNWFZWSlpZUFhaTkdFQUZMWVpTUjJSNVRZNk9ESjNSTTRYV0FDNUVFRiIsIm5hbWUiOiJ0ZXN0Iiwic3ViIjoiVUJHSlhLRkVWUlFEM05LM0lDRVc1Q0lDSzM1NkdESVZORkhaRUU0SzdMMkRYWTdORVNQVlFVNEwiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e30sInN1YnMiOi0xLCJkYXRhIjotMSwicGF5bG9hZCI6LTEsInR5cGUiOiJ1c2VyIiwidmVyc2lvbiI6Mn19.OhPLDZflyJ_keg2xBRDHZZhG5x_Qf_Yb61k9eHLs9zLRf0_ETwMd0PNZI_isuBhXYevobXHVoYA3oxvMVGlDCQ | ||
699 | \\------END NATS USER JWT------ | ||
700 | \\ | ||
701 | \\************************* IMPORTANT ************************* | ||
702 | \\NKEY Seed printed below can be used to sign and prove identity. | ||
703 | \\NKEYs are sensitive and should be treated as secrets. | ||
704 | \\ | ||
705 | \\-----BEGIN USER NKEY SEED----- | ||
706 | \\SUAGIEYODKBBTUMOB666Z5KA4FCWAZV7HWSGRHOD7MK6UM5IYLWLACH7DQ | ||
707 | \\------END USER NKEY SEED------ | ||
708 | \\ | ||
709 | \\************************************************************* | ||
710 | ; | ||
711 | const jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJUWEg1TUxDNTdPTUJUQURYNUJNU0RLWkhSQUtXUFM0TkdHRFFPVlJXRzUyRFdaUlFFVERBIiwiaWF0IjoxNjIxNTgyOTU1LCJpc3MiOiJBQ1ZUQVZMQlFKTklQRjdNWFZWSlpZUFhaTkdFQUZMWVpTUjJSNVRZNk9ESjNSTTRYV0FDNUVFRiIsIm5hbWUiOiJ0ZXN0Iiwic3ViIjoiVUJHSlhLRkVWUlFEM05LM0lDRVc1Q0lDSzM1NkdESVZORkhaRUU0SzdMMkRYWTdORVNQVlFVNEwiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e30sInN1YnMiOi0xLCJkYXRhIjotMSwicGF5bG9hZCI6LTEsInR5cGUiOiJ1c2VyIiwidmVyc2lvbiI6Mn19.OhPLDZflyJ_keg2xBRDHZZhG5x_Qf_Yb61k9eHLs9zLRf0_ETwMd0PNZI_isuBhXYevobXHVoYA3oxvMVGlDCQ"; | ||
712 | const seed = "SUAGIEYODKBBTUMOB666Z5KA4FCWAZV7HWSGRHOD7MK6UM5IYLWLACH7DQ"; | ||
713 | |||
714 | var got_kp = try parseDecoratedUserNkey(creds); | ||
715 | try testing.expectEqualStrings(seed, &got_kp.seedText()); | ||
716 | |||
717 | got_kp = try parseDecoratedNkey(creds); | ||
718 | try testing.expectEqualStrings(seed, &got_kp.seedText()); | ||
719 | |||
720 | var got_jwt = parseDecoratedJwt(creds); | ||
721 | try testing.expectEqualStrings(jwt, got_jwt); | ||
722 | } | ||