aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Rutger Broekhoff2021-05-21 00:16:17 +0200
committerLibravatar Rutger Broekhoff2021-05-21 00:16:17 +0200
commitcfb10566fdb3093363fc39d21a8c5aa5c4deeeeb (patch)
tree80774851a87bf93583bcdfaaf233be32eff227b2 /src
downloadzig-nkeys-cfb10566fdb3093363fc39d21a8c5aa5c4deeeeb.tar.gz
zig-nkeys-cfb10566fdb3093363fc39d21a8c5aa5c4deeeeb.zip
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/base32.zig120
-rw-r--r--src/crc16.zig57
-rw-r--r--src/nkeys.zig518
-rw-r--r--src/znk.zig462
4 files changed, 1157 insertions, 0 deletions
diff --git a/src/base32.zig b/src/base32.zig
new file mode 100644
index 0000000..b1400a5
--- /dev/null
+++ b/src/base32.zig
@@ -0,0 +1,120 @@
1const std = @import("std");
2
3// TODO(rutgerbrf): simplify the code of the encoder & decoder?
4
5pub const Encoder = struct {
6 const Self = @This();
7
8 out_off: u4 = 0,
9 buf: u5 = 0,
10
11 pub fn write(self: *Self, b: u8, out: []u8) usize {
12 var i: usize = 0;
13 var bits_left: u4 = 8;
14 while (bits_left > 0) {
15 var space_avail = @truncate(u3, 5 - self.out_off);
16 var write_bits: u3 = if (bits_left < space_avail) @truncate(u3, bits_left) else space_avail;
17 bits_left -= write_bits;
18 var mask: u8 = (@as(u8, 0x01) << write_bits) - 1;
19 var want: u8 = (b >> @truncate(u3, bits_left)) & mask;
20 self.buf |= @truncate(u5, want << (space_avail - write_bits));
21 self.out_off += write_bits;
22 if (self.out_off == 5) {
23 if (i >= out.len) break;
24 out[i] = self.char();
25 i += 1;
26 self.out_off = 0;
27 self.buf = 0;
28 }
29 }
30 return i;
31 }
32
33 fn char(self: *const Self) u8 {
34 return self.buf + (if (self.buf < 26) @as(u8, 'A') else '2' - 26);
35 }
36};
37
38pub const DecodeError = error{CorruptInputError};
39
40pub const Decoder = struct {
41 const Self = @This();
42
43 out_off: u4 = 0,
44 buf: u8 = 0,
45
46 pub fn read(self: *Self, c: u8) DecodeError!?u8 {
47 var ret: ?u8 = null;
48 var decoded_c = try decodeChar(c);
49 var bits_left: u3 = 5;
50 while (bits_left > 0) {
51 var space_avail: u4 = 8 - self.out_off;
52 var write_bits: u3 = if (bits_left < space_avail) bits_left else @truncate(u3, space_avail);
53 bits_left -= write_bits;
54 var mask: u8 = (@as(u8, 0x01) << write_bits) - 1;
55 var want: u8 = (decoded_c >> bits_left) & mask;
56 self.buf |= want << @truncate(u3, space_avail - write_bits);
57 self.out_off += write_bits;
58 if (self.out_off == 8) {
59 ret = self.buf;
60 self.out_off = 0;
61 self.buf = 0;
62 }
63 }
64 return ret;
65 }
66
67 fn decodeChar(p: u8) DecodeError!u5 {
68 var value: u5 = 0;
69 if (p >= 'A' and p <= 'Z') {
70 value = @truncate(u5, p - @as(u8, 'A'));
71 } else if (p >= '2' and p <= '9') {
72 // '2' -> 26
73 value = @truncate(u5, p - @as(u8, '2') + 26);
74 } else {
75 return error.CorruptInputError;
76 }
77 return value;
78 }
79};
80
81pub fn encodedLen(src_len: usize) usize {
82 const src_len_bits = src_len * 8;
83 return src_len_bits / 5 + (if (src_len_bits % 5 > 0) @as(usize, 1) else 0);
84}
85
86pub fn decodedLen(enc_len: usize) usize {
87 const enc_len_bits = enc_len * 5;
88 return enc_len_bits / 8;
89}
90
91pub fn encode(bs: []const u8, out: []u8) usize {
92 var e = Encoder{};
93 var i: usize = 0;
94 for (bs) |b| {
95 if (i >= out.len) break;
96 i += e.write(b, out[i..]);
97 }
98 if (e.out_off != 0 and i < out.len) {
99 out[i] = e.char();
100 i += 1;
101 }
102 return i; // amount of bytes processed
103}
104
105pub fn decode(ps: []const u8, out: []u8) DecodeError!usize {
106 var d = Decoder{};
107 var i: usize = 0;
108 for (ps) |p| {
109 if (i >= out.len) break;
110 if (try d.read(p)) |b| {
111 out[i] = b;
112 i += 1;
113 }
114 }
115 if (d.out_off != 0 and i < out.len) {
116 out[i] = d.buf;
117 i += 1;
118 }
119 return i; // amount of bytes processed
120}
diff --git a/src/crc16.zig b/src/crc16.zig
new file mode 100644
index 0000000..3b66d99
--- /dev/null
+++ b/src/crc16.zig
@@ -0,0 +1,57 @@
1const Error = error{InvalidChecksum};
2
3// TODO(rutgerbrf): generate this table at compile time?
4const crc16tab = [256]u16{
5 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
6 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
7 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
8 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
9 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
10 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
11 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
12 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
13 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
14 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
15 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
16 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
17 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
18 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
19 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
20 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
21 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
22 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
23 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
24 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
25 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
26 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
27 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
28 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
29 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
30 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
31 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
32 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
33 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
34 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
35 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
36 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,
37};
38
39pub fn update(crc: u16, with_data: []const u8) u16 {
40 var new_crc = crc;
41 for (with_data) |b| {
42 new_crc = ((new_crc << 8) & 0xffff) ^ crc16tab[((new_crc >> 8) ^ @as(u16, b)) & 0x00ff];
43 }
44 return new_crc;
45}
46
47// make returns the CRC16 checksum for the data provided.
48pub fn make(data: []const u8) u16 {
49 return update(0, data);
50}
51
52// validate will check the calculated CRC16 checksum for data against the expected.
53pub fn validate(data: []const u8, expected: u16) !void {
54 if (make(data) != expected) {
55 return error.InvalidChecksum;
56 }
57}
diff --git a/src/nkeys.zig b/src/nkeys.zig
new file mode 100644
index 0000000..7ce44f9
--- /dev/null
+++ b/src/nkeys.zig
@@ -0,0 +1,518 @@
1const std = @import("std");
2const ascii = std.ascii;
3const base32 = @import("base32.zig");
4const crc16 = @import("crc16.zig");
5const crypto = std.crypto;
6const Ed25519 = crypto.sign.Ed25519;
7const mem = std.mem;
8const testing = std.testing;
9
10const Error = error{
11 InvalidPrefixByte,
12 InvalidEncoding,
13 InvalidSeed,
14 NoNKeySeedFound,
15 NoNKeyUserSeedFound,
16};
17
18pub fn fromText(text: []const u8) !Key {
19 if (!isValidEncoding(text)) return error.InvalidEncoding;
20 switch (text[0]) {
21 'S' => {
22 // It's a seed.
23 if (text.len != text_seed_len) return error.InvalidSeed;
24 return Key{ .seed_key_pair = try fromSeed(text[0..text_seed_len]) };
25 },
26 'P' => return error.InvalidEncoding, // unsupported for now
27 else => {
28 if (text.len != text_public_len) return error.InvalidEncoding;
29 return Key{ .public_key = try fromPublicKey(text[0..text_public_len]) };
30 },
31 }
32}
33
34pub const Key = union(enum) {
35 seed_key_pair: SeedKeyPair,
36 public_key: PublicKey,
37
38 const Self = @This();
39
40 pub fn publicKey(self: *const Self) !text_public {
41 return switch (self.*) {
42 .seed_key_pair => |*kp| try kp.publicKey(),
43 .public_key => |*pk| try pk.publicKey(),
44 };
45 }
46
47 pub fn intoPublicKey(self: *const Self) !PublicKey {
48 return switch (self.*) {
49 .seed_key_pair => |*kp| try kp.intoPublicKey(),
50 .public_key => |pk| pk,
51 };
52 }
53
54 pub fn verify(
55 self: *const Self,
56 msg: []const u8,
57 sig: [Ed25519.signature_length]u8,
58 ) !void {
59 return switch (self.*) {
60 .seed_key_pair => |*kp| try kp.verify(msg, sig),
61 .public_key => |*pk| try pk.verify(msg, sig),
62 };
63 }
64
65 pub fn wipe(self: *Self) void {
66 return switch (self.*) {
67 .seed_key_pair => |*kp| kp.wipe(),
68 .public_key => |*pk| pk.wipe(),
69 };
70 }
71};
72
73pub const KeyTypePrefixByte = enum(u8) {
74 seed = 18 << 3, // S
75 private = 15 << 3, // P
76 unknown = 23 << 3, // U
77};
78
79pub const PublicPrefixByte = enum(u8) {
80 account = 0, // A
81 cluster = 2 << 3, // C
82 operator = 14 << 3, // O
83 server = 13 << 3, // N
84 user = 20 << 3, // U
85
86 fn fromU8(b: u8) !PublicPrefixByte {
87 return switch (b) {
88 @enumToInt(PublicPrefixByte.server) => .server,
89 @enumToInt(PublicPrefixByte.cluster) => .cluster,
90 @enumToInt(PublicPrefixByte.operator) => .operator,
91 @enumToInt(PublicPrefixByte.account) => .account,
92 @enumToInt(PublicPrefixByte.user) => .user,
93 else => error.InvalidPrefixByte,
94 };
95 }
96};
97
98pub const SeedKeyPair = struct {
99 const Self = @This();
100
101 seed: text_seed,
102
103 pub fn init(prefix: PublicPrefixByte) !Self {
104 var raw_seed: [Ed25519.seed_length]u8 = undefined;
105 crypto.random.bytes(&raw_seed);
106 defer wipeBytes(&raw_seed);
107
108 var seed = try encodeSeed(prefix, &raw_seed);
109 return Self{ .seed = seed };
110 }
111
112 pub fn initFromSeed(seed: *const text_seed) !Self {
113 var decoded = try decodeSeed(seed);
114 defer decoded.wipe();
115
116 return Self{ .seed = seed.* };
117 }
118
119 fn rawSeed(self: *const Self) ![Ed25519.seed_length]u8 {
120 return (try decodeSeed(&self.seed)).seed;
121 }
122
123 fn keys(self: *const Self) !Ed25519.KeyPair {
124 return Ed25519.KeyPair.create(try rawSeed(self));
125 }
126
127 pub fn privateKey(self: *const Self) !text_private {
128 var kp = try self.keys();
129 defer wipeKeyPair(&kp);
130 return try encodePrivate(&kp.secret_key);
131 }
132
133 pub fn publicKey(self: *const Self) !text_public {
134 var decoded = try decodeSeed(&self.seed);
135 defer decoded.wipe();
136 var kp = try Ed25519.KeyPair.create(decoded.seed);
137 defer wipeKeyPair(&kp);
138 return try encodePublic(decoded.prefix, &kp.public_key);
139 }
140
141 pub fn intoPublicKey(self: *const Self) !PublicKey {
142 var decoded = try decodeSeed(&self.seed);
143 var kp = try Ed25519.KeyPair.create(decoded.seed);
144 defer wipeKeyPair(&kp);
145 return PublicKey{
146 .prefix = decoded.prefix,
147 .key = kp.public_key,
148 };
149 }
150
151 pub fn sign(
152 self: *const Self,
153 msg: []const u8,
154 ) ![Ed25519.signature_length]u8 {
155 var kp = try self.keys();
156 defer wipeKeyPair(&kp);
157 return try Ed25519.sign(msg, kp, null);
158 }
159
160 pub fn verify(
161 self: *const Self,
162 msg: []const u8,
163 sig: [Ed25519.signature_length]u8,
164 ) !void {
165 var kp = try self.keys();
166 defer wipeKeyPair(&kp);
167 try Ed25519.verify(sig, msg, kp.public_key);
168 }
169
170 pub fn wipe(self: *Self) void {
171 wipeBytes(&self.seed);
172 }
173
174 fn wipeKeyPair(kp: *Ed25519.KeyPair) void {
175 wipeBytes(&kp.secret_key);
176 }
177};
178
179fn wipeBytes(bs: []u8) void {
180 for (bs) |*b| b.* = 0;
181}
182
183pub const PublicKey = struct {
184 const Self = @This();
185
186 prefix: PublicPrefixByte,
187 key: [Ed25519.public_length]u8,
188
189 pub fn publicKey(self: *const Self) !text_public {
190 return try encodePublic(self.prefix, &self.key);
191 }
192
193 pub fn verify(
194 self: *const Self,
195 msg: []const u8,
196 sig: [Ed25519.signature_length]u8,
197 ) !void {
198 try Ed25519.verify(sig, msg, self.key);
199 }
200
201 pub fn wipe(self: *Self) void {
202 self.prefix = .user;
203 std.crypto.random.bytes(&self.key);
204 }
205};
206
207// One prefix byte, two CRC bytes
208const binary_private_size = 1 + Ed25519.secret_length + 2;
209// One prefix byte, two CRC bytes
210const binary_public_size = 1 + Ed25519.public_length + 2;
211// Two prefix bytes, two CRC bytes
212const binary_seed_size = 2 + Ed25519.seed_length + 2;
213
214pub const text_private_len = base32.encodedLen(binary_private_size);
215pub const text_public_len = base32.encodedLen(binary_public_size);
216pub const text_seed_len = base32.encodedLen(binary_seed_size);
217
218pub const text_private = [text_private_len]u8;
219pub const text_public = [text_public_len]u8;
220pub const text_seed = [text_seed_len]u8;
221
222pub fn encodePublic(prefix: PublicPrefixByte, key: *const [Ed25519.public_length]u8) !text_public {
223 return encode(1, key.len, &[_]u8{@enumToInt(prefix)}, key);
224}
225
226pub fn encodePrivate(key: *const [Ed25519.secret_length]u8) !text_private {
227 return encode(1, key.len, &[_]u8{@enumToInt(KeyTypePrefixByte.private)}, key);
228}
229
230fn EncodedKey(comptime prefix_len: usize, comptime data_len: usize) type {
231 return [base32.encodedLen(prefix_len + data_len + 2)]u8;
232}
233
234fn encode(
235 comptime prefix_len: usize,
236 comptime data_len: usize,
237 prefix: *const [prefix_len]u8,
238 data: *const [data_len]u8,
239) !EncodedKey(prefix_len, data_len) {
240 var buf: [prefix_len + data_len + 2]u8 = undefined;
241 defer wipeBytes(&buf);
242
243 mem.copy(u8, &buf, prefix[0..]);
244 mem.copy(u8, buf[prefix_len..], data[0..]);
245 var off = prefix_len + data_len;
246 var checksum = crc16.make(buf[0..off]);
247 mem.writeIntLittle(u16, buf[buf.len - 2 .. buf.len], checksum);
248
249 var text: EncodedKey(prefix_len, data_len) = undefined;
250 std.debug.assert(base32.encode(&buf, &text) == text.len);
251
252 return text;
253}
254
255pub fn encodeSeed(prefix: PublicPrefixByte, src: *const [Ed25519.seed_length]u8) !text_seed {
256 var full_prefix = [_]u8{
257 @enumToInt(KeyTypePrefixByte.seed) | (@enumToInt(prefix) >> 5),
258 (@enumToInt(prefix) & 0b00011111) << 3,
259 };
260 return encode(full_prefix.len, src.len, &full_prefix, src);
261}
262
263pub fn decodePrivate(text: *const text_private) ![Ed25519.secret_length]u8 {
264 var decoded = try decode(1, Ed25519.secret_length, text);
265 defer wipeBytes(&decoded.data);
266 if (decoded.prefix[0] != @enumToInt(KeyTypePrefixByte.private))
267 return error.InvalidPrefixByte;
268 return decoded.data;
269}
270
271pub fn decodePublic(prefix: PublicPrefixByte, text: *const text_public) ![Ed25519.public_length]u8 {
272 var decoded = try decode(1, Ed25519.public_length, text);
273 if (decoded.data[0] != @enumToInt(prefix))
274 return error.InvalidPrefixByte;
275 return decoded.data;
276}
277
278fn DecodedNKey(comptime prefix_len: usize, comptime data_len: usize) type {
279 return struct {
280 prefix: [prefix_len]u8,
281 data: [data_len]u8,
282 };
283}
284
285fn decode(
286 comptime prefix_len: usize,
287 comptime data_len: usize,
288 text: *const [base32.encodedLen(prefix_len + data_len + 2)]u8,
289) !DecodedNKey(prefix_len, data_len) {
290 var raw: [prefix_len + data_len + 2]u8 = undefined;
291 defer wipeBytes(&raw);
292 std.debug.assert((try base32.decode(text[0..], &raw)) == raw.len);
293
294 var checksum = mem.readIntLittle(u16, raw[raw.len - 2 .. raw.len]);
295 try crc16.validate(raw[0 .. raw.len - 2], checksum);
296
297 return DecodedNKey(prefix_len, data_len){
298 .prefix = raw[0..prefix_len].*,
299 .data = raw[prefix_len .. raw.len - 2].*,
300 };
301}
302
303pub const DecodedSeed = struct {
304 const Self = @This();
305
306 prefix: PublicPrefixByte,
307 seed: [Ed25519.seed_length]u8,
308
309 pub fn wipe(self: *Self) void {
310 self.prefix = .account;
311 wipeBytes(&self.seed);
312 }
313};
314
315pub fn decodeSeed(text: *const text_seed) !DecodedSeed {
316 var decoded = try decode(2, Ed25519.seed_length, text);
317 defer wipeBytes(&decoded.data); // gets copied
318
319 var key_ty_prefix = decoded.prefix[0] & 0b11111000;
320 var entity_ty_prefix = (decoded.prefix[0] & 0b00000111) << 5 | ((decoded.prefix[1] & 0b11111000) >> 3);
321
322 if (key_ty_prefix != @enumToInt(KeyTypePrefixByte.seed))
323 return error.InvalidSeed;
324
325 return DecodedSeed{
326 .prefix = try PublicPrefixByte.fromU8(entity_ty_prefix),
327 .seed = decoded.data,
328 };
329}
330
331pub fn fromPublicKey(text: *const text_public) !PublicKey {
332 var decoded = try decode(1, Ed25519.public_length, text);
333 defer wipeBytes(&decoded.data); // gets copied
334
335 return PublicKey{
336 .prefix = try PublicPrefixByte.fromU8(decoded.prefix[0]),
337 .key = decoded.data,
338 };
339}
340
341pub fn fromSeed(text: *const text_seed) !SeedKeyPair {
342 var res = try decodeSeed(text);
343 wipeBytes(&res.seed);
344 return SeedKeyPair{ .seed = text.* };
345}
346
347pub fn isValidEncoding(text: []const u8) bool {
348 if (text.len < 4) return false;
349 var made_crc: u16 = 0;
350 var dec = base32.Decoder{};
351 var crc_buf: [2]u8 = undefined;
352 var crc_buf_len: u8 = 0;
353 var expect_len: usize = base32.decodedLen(text.len);
354 var wrote_n_total: usize = 0;
355 for (text) |c, i| {
356 var b = (dec.read(c) catch return false) orelse continue;
357 wrote_n_total += 1;
358 if (crc_buf_len == 2) made_crc = crc16.update(made_crc, &.{crc_buf[0]});
359 crc_buf[0] = crc_buf[1];
360 crc_buf[1] = b;
361 if (crc_buf_len != 2) crc_buf_len += 1;
362 }
363 if (dec.out_off != 0 and wrote_n_total < expect_len) {
364 if (crc_buf_len == 2) made_crc = crc16.update(made_crc, &.{crc_buf[0]});
365 crc_buf[0] = crc_buf[1];
366 crc_buf[1] = dec.buf;
367 if (crc_buf_len != 2) crc_buf_len += 1;
368 }
369 if (crc_buf_len != 2) unreachable;
370 var got_crc = mem.readIntLittle(u16, &crc_buf);
371 return made_crc == got_crc;
372}
373
374pub fn isValidSeed(text: *const text_seed) bool {
375 var res = decodeSeed(text) catch return false;
376 wipeBytes(&res.seed);
377 return true;
378}
379
380pub fn isValidPublicKey(text: *const text_public, with_type: ?PublicPrefixByte) bool {
381 var res = decode(1, Ed25519.public_length, text) catch return false;
382 var public = PublicPrefixByte.fromU8(res.data[0]) catch return false;
383 return if (with_type) |ty| public == ty else true;
384}
385
386pub fn fromRawSeed(prefix: PublicPrefixByte, raw_seed: *const [Ed25519.seed_length]u8) !SeedKeyPair {
387 return SeedKeyPair{ .seed = try encodeSeed(prefix, raw_seed) };
388}
389
390pub fn getNextLine(text: []const u8, off: *usize) ?[]const u8 {
391 if (off.* <= text.len) return null;
392 var newline_pos = mem.indexOfPos(u8, text, off.*, "\n") orelse return null;
393 var start = off.*;
394 var end = newline_pos;
395 if (newline_pos > 0 and text[newline_pos - 1] == '\r') end -= 1;
396 off.* = newline_pos + 1;
397 return text[start..end];
398}
399
400// `line` must not contain CR or LF characters.
401pub fn isKeySectionBarrier(line: []const u8) bool {
402 return line.len >= 6 and mem.startsWith(u8, line, "---") and mem.endsWith(u8, line, "---");
403}
404
405pub fn areKeySectionContentsValid(contents: []const u8) bool {
406 const allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.=";
407
408 for (contents) |c| {
409 var is_c_allowed = false;
410 for (allowed_chars) |allowed_c| {
411 if (c == allowed_c) {
412 is_c_allowed = true;
413 break;
414 }
415 }
416 if (!is_c_allowed) return false;
417 }
418
419 return true;
420}
421
422pub fn findKeySection(text: []const u8, off: *usize) ?[]const u8 {
423 // Skip all space
424 // Lines end with \n, but \r\n is also fine
425 // Contents of the key may consist of abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.=
426 // However, if a line seems to be in the form of ---stuff---, the section is ended.
427 // A newline must be present at the end of the key footer
428 // See https://regex101.com/r/pEaqcJ/1 for a weird edge case in the github.com/nats-io/nkeys library
429 // Another weird edge case: https://regex101.com/r/Xmqj1h/1
430 while (true) {
431 var opening_line = getNextLine(text, off) orelse return null;
432 if (!isKeySectionBarrier(opening_line)) continue;
433
434 var contents_line = getNextLine(text, off) orelse return null;
435 if (!areKeySectionContentsValid(contents_line)) continue;
436
437 var closing_line = getNextLine(text, off) orelse return null;
438 if (!isKeySectionBarrier(closing_line)) continue;
439
440 return contents_line;
441 }
442}
443
444pub fn parseDecoratedJwt(contents: []const u8) ![]const u8 {
445 var current_off: usize = 0;
446 return findKeySection(contents, &current_off) orelse return contents;
447}
448
449fn validNKey(text: []const u8) bool {
450 var valid_prefix =
451 mem.startsWith(u8, text, "SO") or
452 mem.startsWith(u8, text, "SA") or
453 mem.startsWith(u8, text, "SU");
454 var valid_len = text.len >= text_seed_len;
455 return valid_prefix and valid_len;
456}
457
458fn findNKey(text: []const u8) ?[]const u8 {
459 var current_off: usize = 0;
460 while (true) {
461 var line = getNextLine(text, &current_off) orelse return null;
462 for (line) |c, i| {
463 if (!ascii.isSpace(c)) {
464 if (validNKey(line[i..])) return line[i..];
465 break;
466 }
467 }
468 }
469}
470
471pub fn parseDecoratedNKey(contents: []const u8) !SeedKeyPair {
472 var current_off: usize = 0;
473
474 var seed: ?[]const u8 = null;
475 if (findKeySection(contents, &current_off) != null)
476 seed = findKeySection(contents, &current_off);
477 if (seed != null)
478 seed = findNKey(contents) orelse return error.NoNKeySeedFound;
479 if (!validNKey(seed.?))
480 return error.NoNKeySeedFound;
481 return fromSeed(contents[0..text_seed_len]);
482}
483
484pub fn parseDecoratedUserNKey(contents: []const u8) !SeedKeyPair {
485 var key = try parseDecoratedNKey(contents);
486 if (!mem.startsWith(u8, &key.seed, "SU")) return error.NoNKeyUserSeedFound;
487 defer key.wipe();
488 return key;
489}
490
491test {
492 testing.refAllDecls(@This());
493 testing.refAllDecls(Key);
494 testing.refAllDecls(SeedKeyPair);
495 testing.refAllDecls(PublicKey);
496}
497
498test {
499 var key_pair = try SeedKeyPair.init(PublicPrefixByte.server);
500 defer key_pair.wipe();
501
502 var decoded_seed = try decodeSeed(&key_pair.seed);
503 var encoded_second_time = try encodeSeed(decoded_seed.prefix, &decoded_seed.seed);
504 try testing.expectEqualSlices(u8, &key_pair.seed, &encoded_second_time);
505 try testing.expect(isValidEncoding(&key_pair.seed));
506
507 var pub_key_str_a = try key_pair.publicKey();
508 var priv_key_str = try key_pair.privateKey();
509 try testing.expect(pub_key_str_a.len != 0);
510 try testing.expect(priv_key_str.len != 0);
511 try testing.expect(isValidEncoding(&pub_key_str_a));
512 try testing.expect(isValidEncoding(&priv_key_str));
513 wipeBytes(&priv_key_str);
514
515 var pub_key = try key_pair.intoPublicKey();
516 var pub_key_str_b = try pub_key.publicKey();
517 try testing.expectEqualSlices(u8, &pub_key_str_a, &pub_key_str_b);
518}
diff --git a/src/znk.zig b/src/znk.zig
new file mode 100644
index 0000000..ab36c96
--- /dev/null
+++ b/src/znk.zig
@@ -0,0 +1,462 @@
1const std = @import("std");
2const Allocator = mem.Allocator;
3const ascii = std.ascii;
4const build_options = @import("build_options");
5const builtin = std.builtin;
6const fs = std.fs;
7const io = std.io;
8const mem = std.mem;
9const nkeys = @import("nkeys.zig");
10const process = std.process;
11const testing = std.testing;
12
13pub fn fatal(comptime format: []const u8, args: anytype) noreturn {
14 std.debug.print("error: " ++ format ++ "\n", args);
15 process.exit(1);
16}
17
18pub fn info(comptime format: []const u8, args: anytype) void {
19 std.debug.print(format ++ "\n", args);
20}
21
22const usage =
23 \\Usage: znk [command] [options]
24 \\
25 \\Commands:
26 \\
27 \\ gen Generate a new key pair
28 \\ help Print this help and exit
29 \\ sign Sign a file
30 \\ verify Verify a file with a signature
31 \\ version Print version number and exit
32 \\
33 \\General Options:
34 \\
35 \\ -h, --help Print this help and exit
36 \\
37;
38
39var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
40
41pub fn main() anyerror!void {
42 // Stolen from the Zig compiler
43 var gpa_need_deinit = false;
44 const gpa = gpa: {
45 if (!std.builtin.link_libc) {
46 gpa_need_deinit = true;
47 break :gpa &general_purpose_allocator.allocator;
48 }
49 // We would prefer to use raw libc allocator here, but cannot
50 // use it if it won't support the alignment we need.
51 if (@alignOf(std.c.max_align_t) < @alignOf(i128)) {
52 break :gpa std.heap.c_allocator;
53 }
54 break :gpa std.heap.raw_c_allocator;
55 };
56 defer if (gpa_need_deinit) {
57 std.debug.assert(!general_purpose_allocator.deinit());
58 };
59 var arena_instance = std.heap.ArenaAllocator.init(gpa);
60 defer arena_instance.deinit();
61 const arena = &arena_instance.allocator;
62
63 const args = try process.argsAlloc(arena);
64 return mainArgs(gpa, arena, args);
65}
66
67pub fn mainArgs(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
68 if (args.len <= 1) {
69 info("{s}", .{usage});
70 fatal("expected command argument", .{});
71 }
72
73 const cmd = args[1];
74 const cmd_args = args[2..];
75 if (mem.eql(u8, cmd, "gen")) {
76 return cmdGen(gpa, arena, cmd_args);
77 } else if (mem.eql(u8, cmd, "sign")) {
78 return cmdSign(gpa, arena, cmd_args);
79 } else if (mem.eql(u8, cmd, "verify")) {
80 return cmdVerify(gpa, arena, cmd_args);
81 } else if (mem.eql(u8, cmd, "version")) {
82 return io.getStdOut().writeAll(build_options.version ++ "\n");
83 } else if (mem.eql(u8, cmd, "help") or mem.eql(u8, cmd, "-h") or mem.eql(u8, cmd, "--help")) {
84 return io.getStdOut().writeAll(usage);
85 } else {
86 info("{s}", .{usage});
87 fatal("unknown command: {s}", .{cmd});
88 }
89}
90
91const usage_gen =
92 \\Usage: znk gen [options] <type>
93 \\
94 \\Supported Types:
95 \\
96 \\ account
97 \\ cluster
98 \\ operator
99 \\ server
100 \\ user
101 \\
102 \\General Options:
103 \\
104 \\ -h, --help Print this help and exit
105 \\
106 \\Generate Options:
107 \\
108 \\ -o, --pub-out Print the public key to stdout
109 \\ -p, --prefix Vanity public key prefix, turns -o on
110 \\
111;
112
113pub fn cmdGen(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
114 const stdout = io.getStdOut();
115
116 var ty: ?nkeys.PublicPrefixByte = null;
117 var pub_out: bool = false;
118 var prefix: ?[]const u8 = null;
119
120 var i: usize = 0;
121 while (i < args.len) : (i += 1) {
122 const arg = args[i];
123 if (mem.startsWith(u8, arg, "-")) {
124 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
125 return stdout.writeAll(usage_gen);
126 } else if (mem.eql(u8, arg, "-o") or mem.eql(u8, arg, "--pub-out")) {
127 pub_out = true;
128 } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
129 if (i + 1 >= args.len)
130 fatal("expected argument after '{s}'", .{arg});
131 i += 1;
132 if (args[i].len > nkeys.text_public_len - 1)
133 fatal("public key prefix '{s}' is too long", .{arg});
134 prefix = args[i];
135 } else {
136 fatal("unrecognized parameter: '{s}'", .{arg});
137 }
138 } else if (ty != null) {
139 fatal("more than one type to generate provided", .{});
140 } else if (mem.eql(u8, arg, "account")) {
141 ty = .account;
142 } else if (mem.eql(u8, arg, "cluster")) {
143 ty = .cluster;
144 } else if (mem.eql(u8, arg, "operator")) {
145 ty = .operator;
146 } else if (mem.eql(u8, arg, "server")) {
147 ty = .server;
148 } else if (mem.eql(u8, arg, "user")) {
149 ty = .user;
150 } else {
151 fatal("unrecognized extra parameter: '{s}'", .{arg});
152 }
153 }
154
155 if (ty == null) {
156 info("{s}", .{usage_gen});
157 fatal("no type to generate seed for provided", .{});
158 }
159
160 if (prefix != null) {
161 const capitalized_prefix = try toUpper(arena, prefix.?);
162
163 try PrefixKeyGenerator.init(arena, ty.?, capitalized_prefix).generate();
164 } else {
165 var kp = nkeys.SeedKeyPair.init(ty.?) catch |e| fatal("could not generate key pair: {e}", .{e});
166 defer kp.wipe();
167 try stdout.writeAll(&kp.seed);
168 try stdout.writeAll("\n");
169
170 var public_key = kp.publicKey() catch |e| fatal("could not generate public key: {e}", .{e});
171 if (pub_out) {
172 try stdout.writeAll(&public_key);
173 try stdout.writeAll("\n");
174 }
175 }
176}
177
178const usage_sign =
179 \\Usage: znk sign -k <file> [options] <file>
180 \\
181 \\General Options:
182 \\
183 \\ -h, --help Print this help and exit
184 \\
185 \\Sign Options:
186 \\
187 \\ -k, --key Path of private key/seed to sign with
188 \\
189;
190
191pub fn cmdSign(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
192 // TODO(rutgerbrf): support setting a custom entropy file?
193 const stdin = io.getStdIn();
194 const stdout = io.getStdOut();
195
196 var file_stdin = false;
197 var key_stdin = false;
198 var file: ?fs.File = null;
199 var key: ?fs.File = null;
200 defer if (!key_stdin) if (file) |f| f.close();
201 defer if (!file_stdin) if (key) |f| f.close();
202
203 var i: usize = 0;
204 while (i < args.len) : (i += 1) {
205 const arg = args[i];
206 if (mem.startsWith(u8, arg, "-") and arg.len > 1) {
207 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
208 return stdout.writeAll(usage_sign);
209 } else if (mem.eql(u8, arg, "-k") or mem.eql(u8, arg, "--key")) {
210 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
211 i += 1;
212 if (key != null) fatal("parameter '{s}' provided more than once", .{arg});
213 if (std.mem.eql(u8, args[i], "-")) {
214 key = stdin;
215 key_stdin = true;
216 } else {
217 key = try fs.cwd().openFile(args[i], .{});
218 }
219 } else {
220 fatal("unrecognized parameter: '{s}'", .{arg});
221 }
222 } else if (file != null) {
223 fatal("more than one file to generate a signature for provided", .{});
224 } else if (mem.eql(u8, args[i], "-")) {
225 file = stdin;
226 file_stdin = true;
227 } else {
228 file = try fs.cwd().openFile(args[i], .{});
229 }
230 }
231
232 if (file == null) {
233 info("{s}", .{usage_sign});
234 fatal("no file to generate a signature for provided", .{});
235 }
236
237 if (key == null) {
238 info("{s}", .{usage_sign});
239 fatal("no key to sign with provided", .{});
240 }
241
242 if (file_stdin and key_stdin) {
243 fatal("can't use stdin for reading multiple files", .{});
244 }
245
246 const content = file.?.readToEndAlloc(arena, std.math.maxInt(usize)) catch {
247 fatal("could not read file to generate signature for", .{});
248 };
249 var kp = switch (readKeyFile(arena, key.?)) {
250 .seed_key_pair => |kp| kp,
251 else => |*k| {
252 k.wipe();
253 fatal("key was provided but is not a seed", .{});
254 },
255 };
256 defer kp.wipe();
257
258 const sig = kp.sign(content) catch fatal("could not generate signature", .{});
259 var encoded_sig = try arena.alloc(u8, std.base64.standard.Encoder.calcSize(sig.len));
260 _ = std.base64.standard.Encoder.encode(encoded_sig, &sig);
261 try stdout.writeAll(encoded_sig);
262 try stdout.writeAll("\n");
263}
264
265const usage_verify =
266 \\Usage: znk verify [options] <file>
267 \\
268 \\General Options:
269 \\
270 \\ -h, --help Print this help and exit
271 \\
272 \\Verify Options:
273 \\
274 \\ -k, --key Path of key to verify with
275 \\ -s, --sig Path of signature to verify
276 \\
277;
278
279pub fn cmdVerify(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
280 const stdin = io.getStdIn();
281 const stdout = io.getStdOut();
282
283 var file_stdin = false;
284 var key_stdin = false;
285 var sig_stdin = false;
286 var key: ?fs.File = null;
287 var file: ?fs.File = null;
288 var sig: ?fs.File = null;
289 defer if (!file_stdin) if (file) |f| f.close();
290 defer if (!key_stdin) if (key) |f| f.close();
291 defer if (!sig_stdin) if (sig) |f| f.close();
292
293 var i: usize = 0;
294 while (i < args.len) : (i += 1) {
295 const arg = args[i];
296 if (mem.startsWith(u8, arg, "-") and arg.len > 1) {
297 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
298 return stdout.writeAll(usage_verify);
299 } else if (mem.eql(u8, arg, "-k") or mem.eql(u8, arg, "--key")) {
300 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
301 i += 1;
302 if (key != null) fatal("parameter '{s}' provided more than once", .{arg});
303 if (std.mem.eql(u8, args[i], "-")) {
304 key = stdin;
305 key_stdin = true;
306 } else {
307 key = try fs.cwd().openFile(args[i], .{});
308 }
309 } else if (mem.eql(u8, arg, "-s") or mem.eql(u8, arg, "--sig")) {
310 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
311 i += 1;
312 if (sig != null) fatal("parameter '{s}' provided more than once", .{arg});
313 if (std.mem.eql(u8, args[i], "-")) {
314 sig = stdin;
315 sig_stdin = true;
316 } else {
317 sig = try fs.cwd().openFile(args[i], .{});
318 }
319 } else {
320 fatal("unrecognized parameter: '{s}'", .{arg});
321 }
322 } else if (file != null) {
323 fatal("more than one file to verify signature of provided", .{});
324 } else if (mem.eql(u8, args[i], "-")) {
325 file = stdin;
326 file_stdin = true;
327 } else {
328 file = try fs.cwd().openFile(args[i], .{});
329 }
330 }
331
332 if (file == null) {
333 info("{s}", .{usage_verify});
334 fatal("no file to verify signature of provided", .{});
335 }
336
337 if (key == null) {
338 info("{s}", .{usage_verify});
339 fatal("no key to verify signature with provided", .{});
340 }
341
342 if (sig == null) {
343 info("{s}", .{usage_verify});
344 fatal("no file to generate a signature for provided", .{});
345 }
346
347 if (two(&.{ file_stdin, key_stdin, sig_stdin })) {
348 fatal("can't use stdin for reading multiple files", .{});
349 }
350
351 const content = file.?.readToEndAlloc(arena, std.math.maxInt(usize)) catch {
352 fatal("could not read file to generate signature for", .{});
353 };
354 const signature_b64 = sig.?.readToEndAlloc(arena, std.math.maxInt(usize)) catch {
355 fatal("could not read signature", .{});
356 };
357 var k = readKeyFile(arena, key.?);
358 defer k.wipe();
359
360 const trimmed_signature_b64 = mem.trim(u8, signature_b64, " \n\t\r");
361 const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(trimmed_signature_b64) catch {
362 fatal("invalid signature encoding", .{});
363 };
364 if (decoded_len != std.crypto.sign.Ed25519.signature_length)
365 fatal("invalid signature length", .{});
366 const signature = try arena.alloc(u8, decoded_len);
367
368 _ = std.base64.standard.Decoder.decode(signature, trimmed_signature_b64) catch {
369 fatal("invalid signature encoding", .{});
370 };
371 k.verify(content, signature[0..std.crypto.sign.Ed25519.signature_length].*) catch {
372 fatal("bad signature", .{});
373 };
374
375 try stdout.writeAll("good signature\n");
376}
377
378const PrefixKeyGenerator = struct {
379 ty: nkeys.PublicPrefixByte,
380 prefix: []const u8,
381 allocator: *Allocator,
382 done: std.atomic.Bool,
383
384 const Self = @This();
385
386 pub fn init(allocator: *Allocator, ty: nkeys.PublicPrefixByte, prefix: []const u8) Self {
387 return .{
388 .ty = ty,
389 .prefix = prefix,
390 .allocator = allocator,
391 .done = std.atomic.Bool.init(false),
392 };
393 }
394
395 fn generatePrivate(self: *Self) void {
396 while (true) {
397 if (self.done.load(.SeqCst)) return;
398
399 var kp = nkeys.SeedKeyPair.init(self.ty) catch |e| fatal("could not generate key pair: {e}", .{e});
400 defer kp.wipe();
401 var public_key = kp.publicKey() catch |e| fatal("could not generate public key: {e}", .{e});
402 if (!mem.startsWith(u8, public_key[1..], self.prefix)) continue;
403
404 if (self.done.xchg(true, .SeqCst)) return; // another thread is already done
405
406 info("{s}", .{kp.seed});
407 info("{s}", .{public_key});
408
409 return;
410 }
411 }
412
413 pub usingnamespace if (builtin.single_threaded) struct {
414 pub fn generate(self: *Self) !void {
415 return self.generatePrivate();
416 }
417 } else struct {
418 pub fn generate(self: *Self) !void {
419 var cpu_count = try std.Thread.cpuCount();
420 var threads = try self.allocator.alloc(*std.Thread, cpu_count);
421 defer self.allocator.free(threads);
422 for (threads) |*thread| thread.* = try std.Thread.spawn(Self.generatePrivate, self);
423 for (threads) |thread| thread.wait();
424 }
425 };
426};
427
428pub fn readKeyFile(allocator: *Allocator, file: fs.File) nkeys.Key {
429 var bytes = file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch fatal("could not read key file", .{});
430
431 var iterator = mem.split(bytes, "\n");
432 while (iterator.next()) |line| {
433 if (nkeys.isValidEncoding(line) and line.len == nkeys.text_seed_len) {
434 var k = nkeys.fromText(line) catch continue;
435 defer k.wipe();
436 allocator.free(bytes);
437 return k;
438 }
439 }
440
441 fatal("could not find a valid key", .{});
442}
443
444fn two(slice: []const bool) bool {
445 var one = false;
446 for (slice) |x| if (x and one) {
447 return true;
448 } else {
449 one = true;
450 };
451 return false;
452}
453
454fn toUpper(allocator: *Allocator, slice: []const u8) ![]u8 {
455 const result = try allocator.alloc(u8, slice.len);
456 for (slice) |c, i| result[i] = ascii.toUpper(c);
457 return result;
458}
459
460test {
461 testing.refAllDecls(@This());
462}