diff options
| author | Rutger Broekhoff | 2021-05-25 21:37:11 +0200 |
|---|---|---|
| committer | Rutger Broekhoff | 2021-05-25 21:37:11 +0200 |
| commit | 7621f1f91162e18a14562b52e082da0d8a324885 (patch) | |
| tree | ac6e492b72164b71a0cb1f5f46eb3fab857bad8e /src/lib.zig | |
| parent | f78b029e55db9bce054d7b7cb7e290f2a4ade964 (diff) | |
| download | zig-nkeys-7621f1f91162e18a14562b52e082da0d8a324885.tar.gz zig-nkeys-7621f1f91162e18a14562b52e082da0d8a324885.zip | |
Prepare for 0.1.0 release0.1.0
Diffstat (limited to 'src/lib.zig')
| -rw-r--r-- | src/lib.zig | 722 |
1 files changed, 722 insertions, 0 deletions
diff --git a/src/lib.zig b/src/lib.zig new file mode 100644 index 0000000..05922bd --- /dev/null +++ b/src/lib.zig | |||
| @@ -0,0 +1,722 @@ | |||
| 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 | } | ||