8bc756b120d8 — Nathan Michaels 5 years ago
Add secretstream support, using crypto_secretstream_xchacha20poly1305.

TODO: Use actual I/O streams from io.InStream and io.OutStream.
3 files changed, 297 insertions(+), 0 deletions(-)

M README.md
M src/nacl.zig
A => src/secretstream.zig
M README.md +3 -0
@@ 3,3 3,6 @@ A Sodium Wrapper for Zig
 Salty!
 
 It's very incomplete. It's not even plete.
+
+Requires libsodium-dev or, if not on Debian, some package that
+provides sodium.h.

          
M src/nacl.zig +2 -0
@@ 4,7 4,9 @@ const c = @cImport({
 });
 
 pub const crypto_box = @import("crypto_box.zig");
+pub const secretstream = @import("secretstream.zig");
 
 test "nacl" {
     _ = @import("crypto_box.zig");
+    _ = @import("secretstream.zig");
 }

          
A => src/secretstream.zig +292 -0
@@ 0,0 1,292 @@ 
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const ArrayList = std.ArrayList;
+const testing = std.testing;
+
+const c = @cImport({
+    @cInclude("sodium.h");
+});
+
+const StreamError = error{
+    BufferTooShort,
+    ChunkTooBig,
+    EncryptError,
+    InvalidCiphertext,
+    InitError,
+    InvalidHeader,
+};
+
+const Tag = enum {
+    MESSAGE = c.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE,
+    PUSH = c.crypto_secretstream_xchacha20poly1305_TAG_PUSH,
+    REKEY = c.crypto_secretstream_xchacha20poly1305_TAG_REKEY,
+    FINAL = c.crypto_secretstream_xchacha20poly1305_TAG_FINAL,
+};
+
+pub const StreamState = c.crypto_secretstream_xchacha20poly1305_state;
+
+pub const KEYBYTES = c.crypto_secretstream_xchacha20poly1305_KEYBYTES;
+pub const HEADERBYTES = c.crypto_secretstream_xchacha20poly1305_HEADERBYTES;
+pub const ABYTES = c.crypto_secretstream_xchacha20poly1305_ABYTES;
+
+/// Generate a secret key to be used with other secretstream
+/// functions.
+pub fn keygen(key: *[KEYBYTES]u8) void {
+    c.crypto_secretstream_xchacha20poly1305_keygen(key);
+}
+
+/// Initialize state and header with key for writing. Call before
+/// trying to encrypt things.
+pub fn init_push(
+    state: *StreamState,
+    header: *[HEADERBYTES]u8,
+    key: *const [KEYBYTES]u8,
+) !void {
+    const fun = c.crypto_secretstream_xchacha20poly1305_init_push;
+    if (fun(state, header, key) != 0) {
+        return StreamError.InitError;
+    }
+}
+
+/// Push the message into ciphertext. The ciphertext argument must be
+/// at least ABYTES longer than the message.
+pub fn push(
+    state: *StreamState,
+    ciphertext: []u8,
+    message: []const u8,
+    additional_data: ?[]const u8,
+    tag: Tag,
+) !void {
+    var clen: c_ulonglong = undefined;
+    // ciphertext length is guaranteed to always be mlen +
+    // crypto_secretstream_xchacha20poly1305_ABYTES, so let's make
+    // sure there's room.
+    if (ciphertext.len < (message.len + ABYTES)) {
+        return StreamError.BufferTooShort;
+    }
+    const res = c.crypto_secretstream_xchacha20poly1305_push(
+        state,
+        ciphertext.ptr,
+        &clen,
+        message.ptr,
+        message.len,
+        if (additional_data) |ad| ad.ptr else null,
+        if (additional_data) |ad| ad.len else 0,
+        @enumToInt(tag),
+    );
+    if (clen > ciphertext.len) {
+        // I don't trust guarantees that aren't in the source code.
+        return StreamError.BufferTooShort;
+    }
+    if (res != 0) {
+        return StreamError.EncryptError;
+    }
+}
+
+/// Initialize state and header with key for reading. Call before
+/// trying to decrypt things.
+pub fn init_pull(
+    state: *StreamState,
+    header: *const [HEADERBYTES]u8,
+    key: *const [KEYBYTES]u8,
+) !void {
+    const fun = c.crypto_secretstream_xchacha20poly1305_init_pull;
+    if (fun(state, header, key) != 0) {
+        return StreamError.InvalidHeader;
+    }
+}
+
+/// Decrypt bytes from ciphertext and put them in message. The message
+/// argument must be at least (ciphertext.len - ABYTES) bytes long.
+pub fn pull(
+    state: *StreamState,
+    message: []u8,
+    tag: ?*Tag,
+    ciphertext: []const u8,
+    additional_data: ?[]const u8,
+) !void {
+    if (message.len < ciphertext.len - ABYTES) {
+        return StreamError.BufferTooShort;
+    }
+    var mlen: c_ulonglong = undefined;
+    const res = c.crypto_secretstream_xchacha20poly1305_pull(
+        state,
+        message.ptr,
+        &mlen,
+        if (tag) |t| @ptrCast(*u8, t) else null,
+        ciphertext.ptr,
+        ciphertext.len,
+        if (additional_data) |ad| ad.ptr else null,
+        if (additional_data) |ad| ad.len else 0,
+    );
+    if (res != 0) {
+        return StreamError.InvalidCiphertext;
+    }
+    if (mlen > message.len) {
+        return StreamError.BufferTooShort;
+    }
+}
+
+/// Encrypt fixed-sized chunks of data, as many as you
+/// want. Initialize with init, then push until you run out of data to
+/// encrypt. Ciphertext will end up in a dynamically allocated
+/// ArrayList called data. Pass these (in order) along with the header
+/// stored in the hdr member through a ChunkDecrypter to get them
+/// back.
+///
+/// A better way to do this would be to have it accept an output
+/// stream that it can just dump stuff to. That's on the TODO list.
+pub fn ChunkEncrypter(chunk_size: usize) type {
+    return struct {
+        const Self = @This();
+        key: [KEYBYTES]u8,
+        hdr: [HEADERBYTES]u8,
+        state: StreamState,
+        allocator: *Allocator,
+        data: ArrayList([chunk_size + ABYTES]u8),
+
+        /// Create a new encrypter. Push chunks to encrypt.
+        pub fn init(allocator: *Allocator, key: [KEYBYTES]u8) !Self {
+            var st = Self{
+                .key = key,
+                .allocator = allocator,
+                .hdr = undefined,
+                .state = undefined,
+                .data = ArrayList([chunk_size + ABYTES]u8).init(allocator),
+            };
+            try init_push(&st.state, &st.hdr, &key);
+            return st;
+        }
+
+        /// Call with data to encrypt it.
+        pub fn push_chunk(self: *Self, msg: []const u8) !void {
+            if (msg.len > chunk_size) {
+                return StreamError.ChunkTooBig;
+            }
+            // There's probably a better way to do this than to copy
+            // the whole array over to the stack, byte by byte.
+            var m: [chunk_size]u8 = [_]u8{0} ** chunk_size;
+            for (msg) |val, idx| {
+                m[idx] = val;
+            }
+            var ctxt: [chunk_size + ABYTES]u8 = undefined;
+            try push(&self.state, ctxt[0..], m[0..], null, Tag.MESSAGE);
+            try self.data.append(ctxt);
+        }
+
+        /// Free up all held resources.
+        pub fn deinit(self: *Self) void {
+            self.data.deinit();
+        }
+    };
+}
+
+/// ChunkEncrypter's buddy. Initialize with the same key and header
+/// from one, and use it to decrypt chunks.
+pub fn ChunkDecrypter(chunk_size: usize) type {
+    return struct {
+        const Self = @This();
+        key: [KEYBYTES]u8,
+        hdr: [HEADERBYTES]u8,
+        state: StreamState,
+        allocator: *Allocator,
+        data: ArrayList([chunk_size]u8),
+
+        /// Create a new decrypter. Use the header as read from the
+        /// beginning of the ciphertext.
+        pub fn init(
+            allocator: *Allocator,
+            key: [KEYBYTES]u8,
+            hdr: [HEADERBYTES]u8,
+        ) !Self {
+            var st = Self{
+                .key = key,
+                .allocator = allocator,
+                .hdr = hdr,
+                .state = undefined,
+                .data = ArrayList([chunk_size]u8).init(allocator),
+            };
+            try init_pull(&st.state, &st.hdr, &key);
+            return st;
+        }
+
+        /// Decrypt chunks, one at a time. Put decrypted data in
+        /// self.data.
+        pub fn pull_chunk(
+            self: *Self,
+            ciphertext: [chunk_size + ABYTES]u8,
+        ) !void {
+            var msg: [chunk_size]u8 = undefined;
+            try pull(&self.state, msg[0..], null, ciphertext[0..], null);
+            try self.data.append(msg);
+        }
+
+        /// Free all held resources.
+        pub fn deinit(self: *Self) void {
+            self.data.deinit();
+        }
+    };
+}
+
+
+test "stream" {
+    const msg = "Check check wheeee";
+    const msg2 = "Eekers";
+    var key: [KEYBYTES]u8 = undefined;
+    var hdr: [HEADERBYTES]u8 = undefined;
+    var ciphertext: [msg.len + ABYTES]u8 = undefined;
+    var ciphertext2: [msg2.len + ABYTES]u8 = undefined;
+    var state: StreamState = undefined;
+
+    keygen(&key);
+    try init_push(&state, &hdr, &key);
+    try push(&state, ciphertext[0..], msg[0..], null, Tag.MESSAGE);
+    try push(&state, ciphertext2[0..], msg2[0..], null, Tag.MESSAGE);
+
+    var clear: [ciphertext.len - ABYTES]u8 = undefined;
+    var clear2: [ciphertext2.len - ABYTES]u8 = undefined;
+    try init_pull(&state, &hdr, &key);
+    try pull(&state, clear[0..], null, ciphertext[0..], null);
+    try pull(&state, clear2[0..], null, ciphertext2[0..], null);
+    testing.expectEqualSlices(u8, msg[0..], clear[0..]);
+    testing.expectEqualSlices(u8, msg2[0..], clear2[0..]);
+}
+
+test "chunks" {
+    const msg = "This message is longer than my chunk size.";
+    const malloc = std.heap.c_allocator;
+    var key: [KEYBYTES]u8 = undefined;
+
+    keygen(&key);
+    const chunk_size = 4;
+    const Encrypter = ChunkEncrypter(chunk_size);
+    var encrypter = try Encrypter.init(malloc, key);
+    defer encrypter.deinit();
+    var start: usize = 0;
+    while (start < msg.len) : (start += chunk_size) {
+        const off = start + chunk_size;
+        const end = if (off < msg.len) off else msg.len;
+        try encrypter.push_chunk(msg[start..end]);
+    }
+
+    const Decrypter = ChunkDecrypter(chunk_size);
+    var decrypter = try Decrypter.init(malloc, key, encrypter.hdr);
+    defer decrypter.deinit();
+
+    for (encrypter.data.toSlice()) |chunk| {
+        try decrypter.pull_chunk(chunk);
+    }
+
+    const decrypted = try malloc.alloc(u8, decrypter.data.len * chunk_size);
+    defer malloc.free(decrypted);
+
+    for (decrypter.data.toSlice()) |chunk, off| {
+        const base: usize = off * chunk_size;
+        var idx: usize = 0;
+        while (idx < chunk_size) : (idx += 1) {
+            decrypted[idx + base] = chunk[idx];
+        }
+    }
+
+    testing.expectEqualSlices(u8, msg[0..], decrypted[0..msg.len]);
+}