From cfb10566fdb3093363fc39d21a8c5aa5c4deeeeb Mon Sep 17 00:00:00 2001
From: Rutger Broekhoff
Date: Fri, 21 May 2021 00:16:17 +0200
Subject: Initial commit

---
 .gitignore     |   3 +
 LICENSE        | 201 ++++++++++++++++++++++
 README.md      |   8 +
 build.zig      |  49 ++++++
 src/base32.zig | 120 +++++++++++++
 src/crc16.zig  |  57 +++++++
 src/nkeys.zig  | 518 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/znk.zig    | 462 ++++++++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 1418 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 LICENSE
 create mode 100644 README.md
 create mode 100644 build.zig
 create mode 100644 src/base32.zig
 create mode 100644 src/crc16.zig
 create mode 100644 src/nkeys.zig
 create mode 100644 src/znk.zig

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..29534f3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/zig-cache/
+/zig-out/
+.DS_Store
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..16983d5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# NKeys support for Zig
+
+[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
+
+Still a work-in-progress, things will definitely change!
+
+Contains a tool called `znk` which is a clone of the [`nk` tool](https://github.com/nats-io/nkeys/tree/master/nk).
+
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..3053d64
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,49 @@
+const std = @import("std");
+
+pub fn build(b: *std.build.Builder) !void {
+    // Standard target options allows the person running `zig build` to choose
+    // what target to build for. Here we do not override the defaults, which
+    // means any target is allowed, and the default is native. Other options
+    // for restricting supported target set are available.
+    const target = b.standardTargetOptions(.{});
+
+    // Standard release options allow the person running `zig build` to select
+    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
+    const mode = b.standardReleaseOptions();
+
+    const version = "0.1.0-dev";
+
+    const lib = b.addStaticLibrary("zats", "src/main.zig");
+    lib.setBuildMode(mode);
+    lib.addBuildOption([:0]const u8, "version", try b.allocator.dupeZ(u8, version));
+    lib.install();
+
+    var main_tests = b.addTest("src/nkeys.zig");
+    main_tests.setBuildMode(mode);
+    main_tests.addBuildOption([:0]const u8, "version", try b.allocator.dupeZ(u8, version));
+
+    var znk_tests = b.addTest("src/znk.zig");
+    main_tests.setBuildMode(mode);
+    main_tests.addBuildOption([:0]const u8, "version", try b.allocator.dupeZ(u8, version));
+
+    const test_step = b.step("test", "Run library tests");
+    test_step.dependOn(&main_tests.step);
+
+    const znk_test_step = b.step("znk-test", "Run znk tests");
+    znk_test_step.dependOn(&znk_tests.step);
+
+    const exe = b.addExecutable("znk", "src/znk.zig");
+    exe.setTarget(target);
+    exe.setBuildMode(mode);
+    exe.addBuildOption([:0]const u8, "version", try b.allocator.dupeZ(u8, version));
+    exe.install();
+
+    const run_cmd = exe.run();
+    run_cmd.step.dependOn(b.getInstallStep());
+    if (b.args) |args| {
+        run_cmd.addArgs(args);
+    }
+
+    const run_step = b.step("run", "Run znk");
+    run_step.dependOn(&run_cmd.step);
+}
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 @@
+const std = @import("std");
+
+// TODO(rutgerbrf): simplify the code of the encoder & decoder?
+
+pub const Encoder = struct {
+    const Self = @This();
+
+    out_off: u4 = 0,
+    buf: u5 = 0,
+
+    pub fn write(self: *Self, b: u8, out: []u8) usize {
+        var i: usize = 0;
+        var bits_left: u4 = 8;
+        while (bits_left > 0) {
+            var space_avail = @truncate(u3, 5 - self.out_off);
+            var write_bits: u3 = if (bits_left < space_avail) @truncate(u3, bits_left) else space_avail;
+            bits_left -= write_bits;
+            var mask: u8 = (@as(u8, 0x01) << write_bits) - 1;
+            var want: u8 = (b >> @truncate(u3, bits_left)) & mask;
+            self.buf |= @truncate(u5, want << (space_avail - write_bits));
+            self.out_off += write_bits;
+            if (self.out_off == 5) {
+                if (i >= out.len) break;
+                out[i] = self.char();
+                i += 1;
+                self.out_off = 0;
+                self.buf = 0;
+            }
+        }
+        return i;
+    }
+
+    fn char(self: *const Self) u8 {
+        return self.buf + (if (self.buf < 26) @as(u8, 'A') else '2' - 26);
+    }
+};
+
+pub const DecodeError = error{CorruptInputError};
+
+pub const Decoder = struct {
+    const Self = @This();
+
+    out_off: u4 = 0,
+    buf: u8 = 0,
+
+    pub fn read(self: *Self, c: u8) DecodeError!?u8 {
+        var ret: ?u8 = null;
+        var decoded_c = try decodeChar(c);
+        var bits_left: u3 = 5;
+        while (bits_left > 0) {
+            var space_avail: u4 = 8 - self.out_off;
+            var write_bits: u3 = if (bits_left < space_avail) bits_left else @truncate(u3, space_avail);
+            bits_left -= write_bits;
+            var mask: u8 = (@as(u8, 0x01) << write_bits) - 1;
+            var want: u8 = (decoded_c >> bits_left) & mask;
+            self.buf |= want << @truncate(u3, space_avail - write_bits);
+            self.out_off += write_bits;
+            if (self.out_off == 8) {
+                ret = self.buf;
+                self.out_off = 0;
+                self.buf = 0;
+            }
+        }
+        return ret;
+    }
+
+    fn decodeChar(p: u8) DecodeError!u5 {
+        var value: u5 = 0;
+        if (p >= 'A' and p <= 'Z') {
+            value = @truncate(u5, p - @as(u8, 'A'));
+        } else if (p >= '2' and p <= '9') {
+            // '2' -> 26
+            value = @truncate(u5, p - @as(u8, '2') + 26);
+        } else {
+            return error.CorruptInputError;
+        }
+        return value;
+    }
+};
+
+pub fn encodedLen(src_len: usize) usize {
+    const src_len_bits = src_len * 8;
+    return src_len_bits / 5 + (if (src_len_bits % 5 > 0) @as(usize, 1) else 0);
+}
+
+pub fn decodedLen(enc_len: usize) usize {
+    const enc_len_bits = enc_len * 5;
+    return enc_len_bits / 8;
+}
+
+pub fn encode(bs: []const u8, out: []u8) usize {
+    var e = Encoder{};
+    var i: usize = 0;
+    for (bs) |b| {
+        if (i >= out.len) break;
+        i += e.write(b, out[i..]);
+    }
+    if (e.out_off != 0 and i < out.len) {
+        out[i] = e.char();
+        i += 1;
+    }
+    return i; // amount of bytes processed
+}
+
+pub fn decode(ps: []const u8, out: []u8) DecodeError!usize {
+    var d = Decoder{};
+    var i: usize = 0;
+    for (ps) |p| {
+        if (i >= out.len) break;
+        if (try d.read(p)) |b| {
+            out[i] = b;
+            i += 1;
+        }
+    }
+    if (d.out_off != 0 and i < out.len) {
+        out[i] = d.buf;
+        i += 1;
+    }
+    return i; // amount of bytes processed
+}
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 @@
+const Error = error{InvalidChecksum};
+
+// TODO(rutgerbrf): generate this table at compile time?
+const crc16tab = [256]u16{
+    0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
+    0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
+    0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
+    0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
+    0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
+    0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
+    0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
+    0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
+    0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
+    0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
+    0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
+    0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
+    0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
+    0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
+    0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
+    0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
+    0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
+    0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
+    0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
+    0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
+    0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
+    0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
+    0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
+    0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
+    0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
+    0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
+    0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
+    0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
+    0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
+    0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
+    0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
+    0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,
+};
+
+pub fn update(crc: u16, with_data: []const u8) u16 {
+    var new_crc = crc;
+    for (with_data) |b| {
+        new_crc = ((new_crc << 8) & 0xffff) ^ crc16tab[((new_crc >> 8) ^ @as(u16, b)) & 0x00ff];
+    }
+    return new_crc;
+}
+
+// make returns the CRC16 checksum for the data provided.
+pub fn make(data: []const u8) u16 {
+    return update(0, data);
+}
+
+// validate will check the calculated CRC16 checksum for data against the expected.
+pub fn validate(data: []const u8, expected: u16) !void {
+    if (make(data) != expected) {
+        return error.InvalidChecksum;
+    }
+}
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 @@
+const std = @import("std");
+const ascii = std.ascii;
+const base32 = @import("base32.zig");
+const crc16 = @import("crc16.zig");
+const crypto = std.crypto;
+const Ed25519 = crypto.sign.Ed25519;
+const mem = std.mem;
+const testing = std.testing;
+
+const Error = error{
+    InvalidPrefixByte,
+    InvalidEncoding,
+    InvalidSeed,
+    NoNKeySeedFound,
+    NoNKeyUserSeedFound,
+};
+
+pub fn fromText(text: []const u8) !Key {
+    if (!isValidEncoding(text)) return error.InvalidEncoding;
+    switch (text[0]) {
+        'S' => {
+            // It's a seed.
+            if (text.len != text_seed_len) return error.InvalidSeed;
+            return Key{ .seed_key_pair = try fromSeed(text[0..text_seed_len]) };
+        },
+        'P' => return error.InvalidEncoding, // unsupported for now
+        else => {
+            if (text.len != text_public_len) return error.InvalidEncoding;
+            return Key{ .public_key = try fromPublicKey(text[0..text_public_len]) };
+        },
+    }
+}
+
+pub const Key = union(enum) {
+    seed_key_pair: SeedKeyPair,
+    public_key: PublicKey,
+
+    const Self = @This();
+
+    pub fn publicKey(self: *const Self) !text_public {
+        return switch (self.*) {
+            .seed_key_pair => |*kp| try kp.publicKey(),
+            .public_key => |*pk| try pk.publicKey(),
+        };
+    }
+
+    pub fn intoPublicKey(self: *const Self) !PublicKey {
+        return switch (self.*) {
+            .seed_key_pair => |*kp| try kp.intoPublicKey(),
+            .public_key => |pk| pk,
+        };
+    }
+
+    pub fn verify(
+        self: *const Self,
+        msg: []const u8,
+        sig: [Ed25519.signature_length]u8,
+    ) !void {
+        return switch (self.*) {
+            .seed_key_pair => |*kp| try kp.verify(msg, sig),
+            .public_key => |*pk| try pk.verify(msg, sig),
+        };
+    }
+
+    pub fn wipe(self: *Self) void {
+        return switch (self.*) {
+            .seed_key_pair => |*kp| kp.wipe(),
+            .public_key => |*pk| pk.wipe(),
+        };
+    }
+};
+
+pub const KeyTypePrefixByte = enum(u8) {
+    seed = 18 << 3, // S
+    private = 15 << 3, // P
+    unknown = 23 << 3, // U
+};
+
+pub const PublicPrefixByte = enum(u8) {
+    account = 0, // A
+    cluster = 2 << 3, // C
+    operator = 14 << 3, // O
+    server = 13 << 3, // N
+    user = 20 << 3, // U
+
+    fn fromU8(b: u8) !PublicPrefixByte {
+        return switch (b) {
+            @enumToInt(PublicPrefixByte.server) => .server,
+            @enumToInt(PublicPrefixByte.cluster) => .cluster,
+            @enumToInt(PublicPrefixByte.operator) => .operator,
+            @enumToInt(PublicPrefixByte.account) => .account,
+            @enumToInt(PublicPrefixByte.user) => .user,
+            else => error.InvalidPrefixByte,
+        };
+    }
+};
+
+pub const SeedKeyPair = struct {
+    const Self = @This();
+
+    seed: text_seed,
+
+    pub fn init(prefix: PublicPrefixByte) !Self {
+        var raw_seed: [Ed25519.seed_length]u8 = undefined;
+        crypto.random.bytes(&raw_seed);
+        defer wipeBytes(&raw_seed);
+
+        var seed = try encodeSeed(prefix, &raw_seed);
+        return Self{ .seed = seed };
+    }
+
+    pub fn initFromSeed(seed: *const text_seed) !Self {
+        var decoded = try decodeSeed(seed);
+        defer decoded.wipe();
+
+        return Self{ .seed = seed.* };
+    }
+
+    fn rawSeed(self: *const Self) ![Ed25519.seed_length]u8 {
+        return (try decodeSeed(&self.seed)).seed;
+    }
+
+    fn keys(self: *const Self) !Ed25519.KeyPair {
+        return Ed25519.KeyPair.create(try rawSeed(self));
+    }
+
+    pub fn privateKey(self: *const Self) !text_private {
+        var kp = try self.keys();
+        defer wipeKeyPair(&kp);
+        return try encodePrivate(&kp.secret_key);
+    }
+
+    pub fn publicKey(self: *const Self) !text_public {
+        var decoded = try decodeSeed(&self.seed);
+        defer decoded.wipe();
+        var kp = try Ed25519.KeyPair.create(decoded.seed);
+        defer wipeKeyPair(&kp);
+        return try encodePublic(decoded.prefix, &kp.public_key);
+    }
+
+    pub fn intoPublicKey(self: *const Self) !PublicKey {
+        var decoded = try decodeSeed(&self.seed);
+        var kp = try Ed25519.KeyPair.create(decoded.seed);
+        defer wipeKeyPair(&kp);
+        return PublicKey{
+            .prefix = decoded.prefix,
+            .key = kp.public_key,
+        };
+    }
+
+    pub fn sign(
+        self: *const Self,
+        msg: []const u8,
+    ) ![Ed25519.signature_length]u8 {
+        var kp = try self.keys();
+        defer wipeKeyPair(&kp);
+        return try Ed25519.sign(msg, kp, null);
+    }
+
+    pub fn verify(
+        self: *const Self,
+        msg: []const u8,
+        sig: [Ed25519.signature_length]u8,
+    ) !void {
+        var kp = try self.keys();
+        defer wipeKeyPair(&kp);
+        try Ed25519.verify(sig, msg, kp.public_key);
+    }
+
+    pub fn wipe(self: *Self) void {
+        wipeBytes(&self.seed);
+    }
+
+    fn wipeKeyPair(kp: *Ed25519.KeyPair) void {
+        wipeBytes(&kp.secret_key);
+    }
+};
+
+fn wipeBytes(bs: []u8) void {
+    for (bs) |*b| b.* = 0;
+}
+
+pub const PublicKey = struct {
+    const Self = @This();
+
+    prefix: PublicPrefixByte,
+    key: [Ed25519.public_length]u8,
+
+    pub fn publicKey(self: *const Self) !text_public {
+        return try encodePublic(self.prefix, &self.key);
+    }
+
+    pub fn verify(
+        self: *const Self,
+        msg: []const u8,
+        sig: [Ed25519.signature_length]u8,
+    ) !void {
+        try Ed25519.verify(sig, msg, self.key);
+    }
+
+    pub fn wipe(self: *Self) void {
+        self.prefix = .user;
+        std.crypto.random.bytes(&self.key);
+    }
+};
+
+// One prefix byte, two CRC bytes
+const binary_private_size = 1 + Ed25519.secret_length + 2;
+// One prefix byte, two CRC bytes
+const binary_public_size = 1 + Ed25519.public_length + 2;
+// Two prefix bytes, two CRC bytes
+const binary_seed_size = 2 + Ed25519.seed_length + 2;
+
+pub const text_private_len = base32.encodedLen(binary_private_size);
+pub const text_public_len = base32.encodedLen(binary_public_size);
+pub const text_seed_len = base32.encodedLen(binary_seed_size);
+
+pub const text_private = [text_private_len]u8;
+pub const text_public = [text_public_len]u8;
+pub const text_seed = [text_seed_len]u8;
+
+pub fn encodePublic(prefix: PublicPrefixByte, key: *const [Ed25519.public_length]u8) !text_public {
+    return encode(1, key.len, &[_]u8{@enumToInt(prefix)}, key);
+}
+
+pub fn encodePrivate(key: *const [Ed25519.secret_length]u8) !text_private {
+    return encode(1, key.len, &[_]u8{@enumToInt(KeyTypePrefixByte.private)}, key);
+}
+
+fn EncodedKey(comptime prefix_len: usize, comptime data_len: usize) type {
+    return [base32.encodedLen(prefix_len + data_len + 2)]u8;
+}
+
+fn encode(
+    comptime prefix_len: usize,
+    comptime data_len: usize,
+    prefix: *const [prefix_len]u8,
+    data: *const [data_len]u8,
+) !EncodedKey(prefix_len, data_len) {
+    var buf: [prefix_len + data_len + 2]u8 = undefined;
+    defer wipeBytes(&buf);
+
+    mem.copy(u8, &buf, prefix[0..]);
+    mem.copy(u8, buf[prefix_len..], data[0..]);
+    var off = prefix_len + data_len;
+    var checksum = crc16.make(buf[0..off]);
+    mem.writeIntLittle(u16, buf[buf.len - 2 .. buf.len], checksum);
+
+    var text: EncodedKey(prefix_len, data_len) = undefined;
+    std.debug.assert(base32.encode(&buf, &text) == text.len);
+
+    return text;
+}
+
+pub fn encodeSeed(prefix: PublicPrefixByte, src: *const [Ed25519.seed_length]u8) !text_seed {
+    var full_prefix = [_]u8{
+        @enumToInt(KeyTypePrefixByte.seed) | (@enumToInt(prefix) >> 5),
+        (@enumToInt(prefix) & 0b00011111) << 3,
+    };
+    return encode(full_prefix.len, src.len, &full_prefix, src);
+}
+
+pub fn decodePrivate(text: *const text_private) ![Ed25519.secret_length]u8 {
+    var decoded = try decode(1, Ed25519.secret_length, text);
+    defer wipeBytes(&decoded.data);
+    if (decoded.prefix[0] != @enumToInt(KeyTypePrefixByte.private))
+        return error.InvalidPrefixByte;
+    return decoded.data;
+}
+
+pub fn decodePublic(prefix: PublicPrefixByte, text: *const text_public) ![Ed25519.public_length]u8 {
+    var decoded = try decode(1, Ed25519.public_length, text);
+    if (decoded.data[0] != @enumToInt(prefix))
+        return error.InvalidPrefixByte;
+    return decoded.data;
+}
+
+fn DecodedNKey(comptime prefix_len: usize, comptime data_len: usize) type {
+    return struct {
+        prefix: [prefix_len]u8,
+        data: [data_len]u8,
+    };
+}
+
+fn decode(
+    comptime prefix_len: usize,
+    comptime data_len: usize,
+    text: *const [base32.encodedLen(prefix_len + data_len + 2)]u8,
+) !DecodedNKey(prefix_len, data_len) {
+    var raw: [prefix_len + data_len + 2]u8 = undefined;
+    defer wipeBytes(&raw);
+    std.debug.assert((try base32.decode(text[0..], &raw)) == raw.len);
+
+    var checksum = mem.readIntLittle(u16, raw[raw.len - 2 .. raw.len]);
+    try crc16.validate(raw[0 .. raw.len - 2], checksum);
+
+    return DecodedNKey(prefix_len, data_len){
+        .prefix = raw[0..prefix_len].*,
+        .data = raw[prefix_len .. raw.len - 2].*,
+    };
+}
+
+pub const DecodedSeed = struct {
+    const Self = @This();
+
+    prefix: PublicPrefixByte,
+    seed: [Ed25519.seed_length]u8,
+
+    pub fn wipe(self: *Self) void {
+        self.prefix = .account;
+        wipeBytes(&self.seed);
+    }
+};
+
+pub fn decodeSeed(text: *const text_seed) !DecodedSeed {
+    var decoded = try decode(2, Ed25519.seed_length, text);
+    defer wipeBytes(&decoded.data); // gets copied
+
+    var key_ty_prefix = decoded.prefix[0] & 0b11111000;
+    var entity_ty_prefix = (decoded.prefix[0] & 0b00000111) << 5 | ((decoded.prefix[1] & 0b11111000) >> 3);
+
+    if (key_ty_prefix != @enumToInt(KeyTypePrefixByte.seed))
+        return error.InvalidSeed;
+
+    return DecodedSeed{
+        .prefix = try PublicPrefixByte.fromU8(entity_ty_prefix),
+        .seed = decoded.data,
+    };
+}
+
+pub fn fromPublicKey(text: *const text_public) !PublicKey {
+    var decoded = try decode(1, Ed25519.public_length, text);
+    defer wipeBytes(&decoded.data); // gets copied
+
+    return PublicKey{
+        .prefix = try PublicPrefixByte.fromU8(decoded.prefix[0]),
+        .key = decoded.data,
+    };
+}
+
+pub fn fromSeed(text: *const text_seed) !SeedKeyPair {
+    var res = try decodeSeed(text);
+    wipeBytes(&res.seed);
+    return SeedKeyPair{ .seed = text.* };
+}
+
+pub fn isValidEncoding(text: []const u8) bool {
+    if (text.len < 4) return false;
+    var made_crc: u16 = 0;
+    var dec = base32.Decoder{};
+    var crc_buf: [2]u8 = undefined;
+    var crc_buf_len: u8 = 0;
+    var expect_len: usize = base32.decodedLen(text.len);
+    var wrote_n_total: usize = 0;
+    for (text) |c, i| {
+        var b = (dec.read(c) catch return false) orelse continue;
+        wrote_n_total += 1;
+        if (crc_buf_len == 2) made_crc = crc16.update(made_crc, &.{crc_buf[0]});
+        crc_buf[0] = crc_buf[1];
+        crc_buf[1] = b;
+        if (crc_buf_len != 2) crc_buf_len += 1;
+    }
+    if (dec.out_off != 0 and wrote_n_total < expect_len) {
+        if (crc_buf_len == 2) made_crc = crc16.update(made_crc, &.{crc_buf[0]});
+        crc_buf[0] = crc_buf[1];
+        crc_buf[1] = dec.buf;
+        if (crc_buf_len != 2) crc_buf_len += 1;
+    }
+    if (crc_buf_len != 2) unreachable;
+    var got_crc = mem.readIntLittle(u16, &crc_buf);
+    return made_crc == got_crc;
+}
+
+pub fn isValidSeed(text: *const text_seed) bool {
+    var res = decodeSeed(text) catch return false;
+    wipeBytes(&res.seed);
+    return true;
+}
+
+pub fn isValidPublicKey(text: *const text_public, with_type: ?PublicPrefixByte) bool {
+    var res = decode(1, Ed25519.public_length, text) catch return false;
+    var public = PublicPrefixByte.fromU8(res.data[0]) catch return false;
+    return if (with_type) |ty| public == ty else true;
+}
+
+pub fn fromRawSeed(prefix: PublicPrefixByte, raw_seed: *const [Ed25519.seed_length]u8) !SeedKeyPair {
+    return SeedKeyPair{ .seed = try encodeSeed(prefix, raw_seed) };
+}
+
+pub fn getNextLine(text: []const u8, off: *usize) ?[]const u8 {
+    if (off.* <= text.len) return null;
+    var newline_pos = mem.indexOfPos(u8, text, off.*, "\n") orelse return null;
+    var start = off.*;
+    var end = newline_pos;
+    if (newline_pos > 0 and text[newline_pos - 1] == '\r') end -= 1;
+    off.* = newline_pos + 1;
+    return text[start..end];
+}
+
+// `line` must not contain CR or LF characters.
+pub fn isKeySectionBarrier(line: []const u8) bool {
+    return line.len >= 6 and mem.startsWith(u8, line, "---") and mem.endsWith(u8, line, "---");
+}
+
+pub fn areKeySectionContentsValid(contents: []const u8) bool {
+    const allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.=";
+
+    for (contents) |c| {
+        var is_c_allowed = false;
+        for (allowed_chars) |allowed_c| {
+            if (c == allowed_c) {
+                is_c_allowed = true;
+                break;
+            }
+        }
+        if (!is_c_allowed) return false;
+    }
+
+    return true;
+}
+
+pub fn findKeySection(text: []const u8, off: *usize) ?[]const u8 {
+    // Skip all space
+    // Lines end with \n, but \r\n is also fine
+    // Contents of the key may consist of abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.=
+    // However, if a line seems to be in the form of ---stuff---, the section is ended.
+    // A newline must be present at the end of the key footer
+    // See https://regex101.com/r/pEaqcJ/1 for a weird edge case in the github.com/nats-io/nkeys library
+    // Another weird edge case: https://regex101.com/r/Xmqj1h/1
+    while (true) {
+        var opening_line = getNextLine(text, off) orelse return null;
+        if (!isKeySectionBarrier(opening_line)) continue;
+
+        var contents_line = getNextLine(text, off) orelse return null;
+        if (!areKeySectionContentsValid(contents_line)) continue;
+
+        var closing_line = getNextLine(text, off) orelse return null;
+        if (!isKeySectionBarrier(closing_line)) continue;
+
+        return contents_line;
+    }
+}
+
+pub fn parseDecoratedJwt(contents: []const u8) ![]const u8 {
+    var current_off: usize = 0;
+    return findKeySection(contents, &current_off) orelse return contents;
+}
+
+fn validNKey(text: []const u8) bool {
+    var valid_prefix =
+        mem.startsWith(u8, text, "SO") or
+        mem.startsWith(u8, text, "SA") or
+        mem.startsWith(u8, text, "SU");
+    var valid_len = text.len >= text_seed_len;
+    return valid_prefix and valid_len;
+}
+
+fn findNKey(text: []const u8) ?[]const u8 {
+    var current_off: usize = 0;
+    while (true) {
+        var line = getNextLine(text, &current_off) orelse return null;
+        for (line) |c, i| {
+            if (!ascii.isSpace(c)) {
+                if (validNKey(line[i..])) return line[i..];
+                break;
+            }
+        }
+    }
+}
+
+pub fn parseDecoratedNKey(contents: []const u8) !SeedKeyPair {
+    var current_off: usize = 0;
+
+    var seed: ?[]const u8 = null;
+    if (findKeySection(contents, &current_off) != null)
+        seed = findKeySection(contents, &current_off);
+    if (seed != null)
+        seed = findNKey(contents) orelse return error.NoNKeySeedFound;
+    if (!validNKey(seed.?))
+        return error.NoNKeySeedFound;
+    return fromSeed(contents[0..text_seed_len]);
+}
+
+pub fn parseDecoratedUserNKey(contents: []const u8) !SeedKeyPair {
+    var key = try parseDecoratedNKey(contents);
+    if (!mem.startsWith(u8, &key.seed, "SU")) return error.NoNKeyUserSeedFound;
+    defer key.wipe();
+    return key;
+}
+
+test {
+    testing.refAllDecls(@This());
+    testing.refAllDecls(Key);
+    testing.refAllDecls(SeedKeyPair);
+    testing.refAllDecls(PublicKey);
+}
+
+test {
+    var key_pair = try SeedKeyPair.init(PublicPrefixByte.server);
+    defer key_pair.wipe();
+
+    var decoded_seed = try decodeSeed(&key_pair.seed);
+    var encoded_second_time = try encodeSeed(decoded_seed.prefix, &decoded_seed.seed);
+    try testing.expectEqualSlices(u8, &key_pair.seed, &encoded_second_time);
+    try testing.expect(isValidEncoding(&key_pair.seed));
+
+    var pub_key_str_a = try key_pair.publicKey();
+    var priv_key_str = try key_pair.privateKey();
+    try testing.expect(pub_key_str_a.len != 0);
+    try testing.expect(priv_key_str.len != 0);
+    try testing.expect(isValidEncoding(&pub_key_str_a));
+    try testing.expect(isValidEncoding(&priv_key_str));
+    wipeBytes(&priv_key_str);
+
+    var pub_key = try key_pair.intoPublicKey();
+    var pub_key_str_b = try pub_key.publicKey();
+    try testing.expectEqualSlices(u8, &pub_key_str_a, &pub_key_str_b);
+}
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 @@
+const std = @import("std");
+const Allocator = mem.Allocator;
+const ascii = std.ascii;
+const build_options = @import("build_options");
+const builtin = std.builtin;
+const fs = std.fs;
+const io = std.io;
+const mem = std.mem;
+const nkeys = @import("nkeys.zig");
+const process = std.process;
+const testing = std.testing;
+
+pub fn fatal(comptime format: []const u8, args: anytype) noreturn {
+    std.debug.print("error: " ++ format ++ "\n", args);
+    process.exit(1);
+}
+
+pub fn info(comptime format: []const u8, args: anytype) void {
+    std.debug.print(format ++ "\n", args);
+}
+
+const usage =
+    \\Usage: znk [command] [options]
+    \\
+    \\Commands:
+    \\
+    \\  gen            Generate a new key pair
+    \\  help           Print this help and exit
+    \\  sign           Sign a file
+    \\  verify         Verify a file with a signature
+    \\  version        Print version number and exit
+    \\
+    \\General Options:
+    \\
+    \\  -h, --help     Print this help and exit
+    \\
+;
+
+var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
+
+pub fn main() anyerror!void {
+    // Stolen from the Zig compiler
+    var gpa_need_deinit = false;
+    const gpa = gpa: {
+        if (!std.builtin.link_libc) {
+            gpa_need_deinit = true;
+            break :gpa &general_purpose_allocator.allocator;
+        }
+        // We would prefer to use raw libc allocator here, but cannot
+        // use it if it won't support the alignment we need.
+        if (@alignOf(std.c.max_align_t) < @alignOf(i128)) {
+            break :gpa std.heap.c_allocator;
+        }
+        break :gpa std.heap.raw_c_allocator;
+    };
+    defer if (gpa_need_deinit) {
+        std.debug.assert(!general_purpose_allocator.deinit());
+    };
+    var arena_instance = std.heap.ArenaAllocator.init(gpa);
+    defer arena_instance.deinit();
+    const arena = &arena_instance.allocator;
+
+    const args = try process.argsAlloc(arena);
+    return mainArgs(gpa, arena, args);
+}
+
+pub fn mainArgs(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
+    if (args.len <= 1) {
+        info("{s}", .{usage});
+        fatal("expected command argument", .{});
+    }
+
+    const cmd = args[1];
+    const cmd_args = args[2..];
+    if (mem.eql(u8, cmd, "gen")) {
+        return cmdGen(gpa, arena, cmd_args);
+    } else if (mem.eql(u8, cmd, "sign")) {
+        return cmdSign(gpa, arena, cmd_args);
+    } else if (mem.eql(u8, cmd, "verify")) {
+        return cmdVerify(gpa, arena, cmd_args);
+    } else if (mem.eql(u8, cmd, "version")) {
+        return io.getStdOut().writeAll(build_options.version ++ "\n");
+    } else if (mem.eql(u8, cmd, "help") or mem.eql(u8, cmd, "-h") or mem.eql(u8, cmd, "--help")) {
+        return io.getStdOut().writeAll(usage);
+    } else {
+        info("{s}", .{usage});
+        fatal("unknown command: {s}", .{cmd});
+    }
+}
+
+const usage_gen =
+    \\Usage: znk gen [options] <type>
+    \\
+    \\Supported Types:
+    \\
+    \\  account
+    \\  cluster
+    \\  operator
+    \\  server
+    \\  user
+    \\
+    \\General Options:
+    \\
+    \\  -h, --help     Print this help and exit
+    \\
+    \\Generate Options:
+    \\
+    \\  -o, --pub-out  Print the public key to stdout
+    \\  -p, --prefix   Vanity public key prefix, turns -o on
+    \\
+;
+
+pub fn cmdGen(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
+    const stdout = io.getStdOut();
+
+    var ty: ?nkeys.PublicPrefixByte = null;
+    var pub_out: bool = false;
+    var prefix: ?[]const u8 = null;
+
+    var i: usize = 0;
+    while (i < args.len) : (i += 1) {
+        const arg = args[i];
+        if (mem.startsWith(u8, arg, "-")) {
+            if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
+                return stdout.writeAll(usage_gen);
+            } else if (mem.eql(u8, arg, "-o") or mem.eql(u8, arg, "--pub-out")) {
+                pub_out = true;
+            } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
+                if (i + 1 >= args.len)
+                    fatal("expected argument after '{s}'", .{arg});
+                i += 1;
+                if (args[i].len > nkeys.text_public_len - 1)
+                    fatal("public key prefix '{s}' is too long", .{arg});
+                prefix = args[i];
+            } else {
+                fatal("unrecognized parameter: '{s}'", .{arg});
+            }
+        } else if (ty != null) {
+            fatal("more than one type to generate provided", .{});
+        } else if (mem.eql(u8, arg, "account")) {
+            ty = .account;
+        } else if (mem.eql(u8, arg, "cluster")) {
+            ty = .cluster;
+        } else if (mem.eql(u8, arg, "operator")) {
+            ty = .operator;
+        } else if (mem.eql(u8, arg, "server")) {
+            ty = .server;
+        } else if (mem.eql(u8, arg, "user")) {
+            ty = .user;
+        } else {
+            fatal("unrecognized extra parameter: '{s}'", .{arg});
+        }
+    }
+
+    if (ty == null) {
+        info("{s}", .{usage_gen});
+        fatal("no type to generate seed for provided", .{});
+    }
+
+    if (prefix != null) {
+        const capitalized_prefix = try toUpper(arena, prefix.?);
+
+        try PrefixKeyGenerator.init(arena, ty.?, capitalized_prefix).generate();
+    } else {
+        var kp = nkeys.SeedKeyPair.init(ty.?) catch |e| fatal("could not generate key pair: {e}", .{e});
+        defer kp.wipe();
+        try stdout.writeAll(&kp.seed);
+        try stdout.writeAll("\n");
+
+        var public_key = kp.publicKey() catch |e| fatal("could not generate public key: {e}", .{e});
+        if (pub_out) {
+            try stdout.writeAll(&public_key);
+            try stdout.writeAll("\n");
+        }
+    }
+}
+
+const usage_sign =
+    \\Usage: znk sign -k <file> [options] <file>
+    \\
+    \\General Options:
+    \\
+    \\  -h, --help     Print this help and exit
+    \\
+    \\Sign Options:
+    \\
+    \\  -k, --key      Path of private key/seed to sign with
+    \\
+;
+
+pub fn cmdSign(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
+    // TODO(rutgerbrf): support setting a custom entropy file?
+    const stdin = io.getStdIn();
+    const stdout = io.getStdOut();
+
+    var file_stdin = false;
+    var key_stdin = false;
+    var file: ?fs.File = null;
+    var key: ?fs.File = null;
+    defer if (!key_stdin) if (file) |f| f.close();
+    defer if (!file_stdin) if (key) |f| f.close();
+
+    var i: usize = 0;
+    while (i < args.len) : (i += 1) {
+        const arg = args[i];
+        if (mem.startsWith(u8, arg, "-") and arg.len > 1) {
+            if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
+                return stdout.writeAll(usage_sign);
+            } else if (mem.eql(u8, arg, "-k") or mem.eql(u8, arg, "--key")) {
+                if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
+                i += 1;
+                if (key != null) fatal("parameter '{s}' provided more than once", .{arg});
+                if (std.mem.eql(u8, args[i], "-")) {
+                    key = stdin;
+                    key_stdin = true;
+                } else {
+                    key = try fs.cwd().openFile(args[i], .{});
+                }
+            } else {
+                fatal("unrecognized parameter: '{s}'", .{arg});
+            }
+        } else if (file != null) {
+            fatal("more than one file to generate a signature for provided", .{});
+        } else if (mem.eql(u8, args[i], "-")) {
+            file = stdin;
+            file_stdin = true;
+        } else {
+            file = try fs.cwd().openFile(args[i], .{});
+        }
+    }
+
+    if (file == null) {
+        info("{s}", .{usage_sign});
+        fatal("no file to generate a signature for provided", .{});
+    }
+
+    if (key == null) {
+        info("{s}", .{usage_sign});
+        fatal("no key to sign with provided", .{});
+    }
+
+    if (file_stdin and key_stdin) {
+        fatal("can't use stdin for reading multiple files", .{});
+    }
+
+    const content = file.?.readToEndAlloc(arena, std.math.maxInt(usize)) catch {
+        fatal("could not read file to generate signature for", .{});
+    };
+    var kp = switch (readKeyFile(arena, key.?)) {
+        .seed_key_pair => |kp| kp,
+        else => |*k| {
+            k.wipe();
+            fatal("key was provided but is not a seed", .{});
+        },
+    };
+    defer kp.wipe();
+
+    const sig = kp.sign(content) catch fatal("could not generate signature", .{});
+    var encoded_sig = try arena.alloc(u8, std.base64.standard.Encoder.calcSize(sig.len));
+    _ = std.base64.standard.Encoder.encode(encoded_sig, &sig);
+    try stdout.writeAll(encoded_sig);
+    try stdout.writeAll("\n");
+}
+
+const usage_verify =
+    \\Usage: znk verify [options] <file>
+    \\
+    \\General Options:
+    \\
+    \\  -h, --help     Print this help and exit
+    \\
+    \\Verify Options:
+    \\
+    \\  -k, --key      Path of key to verify with
+    \\  -s, --sig      Path of signature to verify
+    \\
+;
+
+pub fn cmdVerify(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
+    const stdin = io.getStdIn();
+    const stdout = io.getStdOut();
+
+    var file_stdin = false;
+    var key_stdin = false;
+    var sig_stdin = false;
+    var key: ?fs.File = null;
+    var file: ?fs.File = null;
+    var sig: ?fs.File = null;
+    defer if (!file_stdin) if (file) |f| f.close();
+    defer if (!key_stdin) if (key) |f| f.close();
+    defer if (!sig_stdin) if (sig) |f| f.close();
+
+    var i: usize = 0;
+    while (i < args.len) : (i += 1) {
+        const arg = args[i];
+        if (mem.startsWith(u8, arg, "-") and arg.len > 1) {
+            if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
+                return stdout.writeAll(usage_verify);
+            } else if (mem.eql(u8, arg, "-k") or mem.eql(u8, arg, "--key")) {
+                if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
+                i += 1;
+                if (key != null) fatal("parameter '{s}' provided more than once", .{arg});
+                if (std.mem.eql(u8, args[i], "-")) {
+                    key = stdin;
+                    key_stdin = true;
+                } else {
+                    key = try fs.cwd().openFile(args[i], .{});
+                }
+            } else if (mem.eql(u8, arg, "-s") or mem.eql(u8, arg, "--sig")) {
+                if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
+                i += 1;
+                if (sig != null) fatal("parameter '{s}' provided more than once", .{arg});
+                if (std.mem.eql(u8, args[i], "-")) {
+                    sig = stdin;
+                    sig_stdin = true;
+                } else {
+                    sig = try fs.cwd().openFile(args[i], .{});
+                }
+            } else {
+                fatal("unrecognized parameter: '{s}'", .{arg});
+            }
+        } else if (file != null) {
+            fatal("more than one file to verify signature of provided", .{});
+        } else if (mem.eql(u8, args[i], "-")) {
+            file = stdin;
+            file_stdin = true;
+        } else {
+            file = try fs.cwd().openFile(args[i], .{});
+        }
+    }
+
+    if (file == null) {
+        info("{s}", .{usage_verify});
+        fatal("no file to verify signature of provided", .{});
+    }
+
+    if (key == null) {
+        info("{s}", .{usage_verify});
+        fatal("no key to verify signature with provided", .{});
+    }
+
+    if (sig == null) {
+        info("{s}", .{usage_verify});
+        fatal("no file to generate a signature for provided", .{});
+    }
+
+    if (two(&.{ file_stdin, key_stdin, sig_stdin })) {
+        fatal("can't use stdin for reading multiple files", .{});
+    }
+
+    const content = file.?.readToEndAlloc(arena, std.math.maxInt(usize)) catch {
+        fatal("could not read file to generate signature for", .{});
+    };
+    const signature_b64 = sig.?.readToEndAlloc(arena, std.math.maxInt(usize)) catch {
+        fatal("could not read signature", .{});
+    };
+    var k = readKeyFile(arena, key.?);
+    defer k.wipe();
+
+    const trimmed_signature_b64 = mem.trim(u8, signature_b64, " \n\t\r");
+    const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(trimmed_signature_b64) catch {
+        fatal("invalid signature encoding", .{});
+    };
+    if (decoded_len != std.crypto.sign.Ed25519.signature_length)
+        fatal("invalid signature length", .{});
+    const signature = try arena.alloc(u8, decoded_len);
+
+    _ = std.base64.standard.Decoder.decode(signature, trimmed_signature_b64) catch {
+        fatal("invalid signature encoding", .{});
+    };
+    k.verify(content, signature[0..std.crypto.sign.Ed25519.signature_length].*) catch {
+        fatal("bad signature", .{});
+    };
+
+    try stdout.writeAll("good signature\n");
+}
+
+const PrefixKeyGenerator = struct {
+    ty: nkeys.PublicPrefixByte,
+    prefix: []const u8,
+    allocator: *Allocator,
+    done: std.atomic.Bool,
+
+    const Self = @This();
+
+    pub fn init(allocator: *Allocator, ty: nkeys.PublicPrefixByte, prefix: []const u8) Self {
+        return .{
+            .ty = ty,
+            .prefix = prefix,
+            .allocator = allocator,
+            .done = std.atomic.Bool.init(false),
+        };
+    }
+
+    fn generatePrivate(self: *Self) void {
+        while (true) {
+            if (self.done.load(.SeqCst)) return;
+
+            var kp = nkeys.SeedKeyPair.init(self.ty) catch |e| fatal("could not generate key pair: {e}", .{e});
+            defer kp.wipe();
+            var public_key = kp.publicKey() catch |e| fatal("could not generate public key: {e}", .{e});
+            if (!mem.startsWith(u8, public_key[1..], self.prefix)) continue;
+
+            if (self.done.xchg(true, .SeqCst)) return; // another thread is already done
+
+            info("{s}", .{kp.seed});
+            info("{s}", .{public_key});
+
+            return;
+        }
+    }
+
+    pub usingnamespace if (builtin.single_threaded) struct {
+        pub fn generate(self: *Self) !void {
+            return self.generatePrivate();
+        }
+    } else struct {
+        pub fn generate(self: *Self) !void {
+            var cpu_count = try std.Thread.cpuCount();
+            var threads = try self.allocator.alloc(*std.Thread, cpu_count);
+            defer self.allocator.free(threads);
+            for (threads) |*thread| thread.* = try std.Thread.spawn(Self.generatePrivate, self);
+            for (threads) |thread| thread.wait();
+        }
+    };
+};
+
+pub fn readKeyFile(allocator: *Allocator, file: fs.File) nkeys.Key {
+    var bytes = file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch fatal("could not read key file", .{});
+
+    var iterator = mem.split(bytes, "\n");
+    while (iterator.next()) |line| {
+        if (nkeys.isValidEncoding(line) and line.len == nkeys.text_seed_len) {
+            var k = nkeys.fromText(line) catch continue;
+            defer k.wipe();
+            allocator.free(bytes);
+            return k;
+        }
+    }
+
+    fatal("could not find a valid key", .{});
+}
+
+fn two(slice: []const bool) bool {
+    var one = false;
+    for (slice) |x| if (x and one) {
+        return true;
+    } else {
+        one = true;
+    };
+    return false;
+}
+
+fn toUpper(allocator: *Allocator, slice: []const u8) ![]u8 {
+    const result = try allocator.alloc(u8, slice.len);
+    for (slice) |c, i| result[i] = ascii.toUpper(c);
+    return result;
+}
+
+test {
+    testing.refAllDecls(@This());
+}
-- 
cgit v1.2.3