Commit 795033c35f

Frank Denis <github@pureftpd.org>
2020-08-16 21:33:02
std/crypto: XChaCha20, detached modes and standard AEAD API
* Factor redundant code in std/crypto/chacha20 * Add support for XChaCha20, and the XChaCha20-Poly1305 construction. XChaCha20 is a 24-byte version of ChaCha20, is widely implemented and is on the standards track: https://tools.ietf.org/html/draft-irtf-cfrg-xchacha-03 * Add support for encryption/decryption with the authentication tag detached from the ciphertext * Add wrappers with an API similar to the Gimli AEAD type, so that we can use and benchmark AEADs with a common API.
1 parent 5cb9668
Changed files (1)
lib
std
lib/std/crypto/chacha20.zig
@@ -25,12 +25,24 @@ fn Rp(a: usize, b: usize, c: usize, d: usize) QuarterRound {
     };
 }
 
-// The chacha family of ciphers are based on the salsa family.
-fn salsa20_wordtobyte(out: []u8, input: [16]u32) void {
-    assert(out.len >= 64);
+fn initContext(key: [8]u32, d: [4]u32) [16]u32 {
+    var ctx: [16]u32 = undefined;
+    const c = "expand 32-byte k";
+    const constant_le = comptime [_]u32{
+        mem.readIntLittle(u32, c[0..4]),
+        mem.readIntLittle(u32, c[4..8]),
+        mem.readIntLittle(u32, c[8..12]),
+        mem.readIntLittle(u32, c[12..16]),
+    };
+    mem.copy(u32, ctx[0..], constant_le[0..4]);
+    mem.copy(u32, ctx[4..12], key[0..8]);
+    mem.copy(u32, ctx[12..16], d[0..4]);
 
-    var x: [16]u32 = undefined;
+    return ctx;
+}
 
+// The chacha family of ciphers are based on the salsa family.
+fn chacha20Core(x: []u32, input: [16]u32) void {
     for (x) |_, i|
         x[i] = input[i];
 
@@ -59,33 +71,27 @@ fn salsa20_wordtobyte(out: []u8, input: [16]u32) void {
             x[r.b] = std.math.rotl(u32, x[r.b] ^ x[r.c], @as(u32, 7));
         }
     }
+}
 
+fn hashToBytes(out: []u8, x: [16]u32) void {
     for (x) |_, i| {
-        mem.writeIntLittle(u32, out[4 * i ..][0..4], x[i] +% input[i]);
+        mem.writeIntLittle(u32, out[4 * i ..][0..4], x[i]);
     }
 }
 
 fn chaCha20_internal(out: []u8, in: []const u8, key: [8]u32, counter: [4]u32) void {
-    var ctx: [16]u32 = undefined;
+    var ctx = initContext(key, counter);
     var remaining: usize = if (in.len > out.len) in.len else out.len;
     var cursor: usize = 0;
 
-    const c = "expand 32-byte k";
-    const constant_le = [_]u32{
-        mem.readIntLittle(u32, c[0..4]),
-        mem.readIntLittle(u32, c[4..8]),
-        mem.readIntLittle(u32, c[8..12]),
-        mem.readIntLittle(u32, c[12..16]),
-    };
-
-    mem.copy(u32, ctx[0..], constant_le[0..4]);
-    mem.copy(u32, ctx[4..12], key[0..8]);
-    mem.copy(u32, ctx[12..16], counter[0..4]);
-
     while (true) {
+        var x: [16]u32 = undefined;
         var buf: [64]u8 = undefined;
-        salsa20_wordtobyte(buf[0..], ctx);
-
+        chacha20Core(x[0..], ctx);
+        for (x) |_, i| {
+            x[i] +%= ctx[i];
+        }
+        hashToBytes(buf[0..], x);
         if (remaining < 64) {
             var i: usize = 0;
             while (i < remaining) : (i += 1)
@@ -104,6 +110,20 @@ fn chaCha20_internal(out: []u8, in: []const u8, key: [8]u32, counter: [4]u32) vo
     }
 }
 
+fn keyToWords(key: [32]u8) [8]u32 {
+    var k: [8]u32 = undefined;
+    k[0] = mem.readIntLittle(u32, key[0..4]);
+    k[1] = mem.readIntLittle(u32, key[4..8]);
+    k[2] = mem.readIntLittle(u32, key[8..12]);
+    k[3] = mem.readIntLittle(u32, key[12..16]);
+    k[4] = mem.readIntLittle(u32, key[16..20]);
+    k[5] = mem.readIntLittle(u32, key[20..24]);
+    k[6] = mem.readIntLittle(u32, key[24..28]);
+    k[7] = mem.readIntLittle(u32, key[28..32]);
+
+    return k;
+}
+
 /// ChaCha20 avoids the possibility of timing attacks, as there are no branches
 /// on secret key data.
 ///
@@ -116,23 +136,12 @@ pub fn chaCha20IETF(out: []u8, in: []const u8, counter: u32, key: [32]u8, nonce:
     assert(in.len >= out.len);
     assert((in.len >> 6) + counter <= maxInt(u32));
 
-    var k: [8]u32 = undefined;
     var c: [4]u32 = undefined;
-
-    k[0] = mem.readIntLittle(u32, key[0..4]);
-    k[1] = mem.readIntLittle(u32, key[4..8]);
-    k[2] = mem.readIntLittle(u32, key[8..12]);
-    k[3] = mem.readIntLittle(u32, key[12..16]);
-    k[4] = mem.readIntLittle(u32, key[16..20]);
-    k[5] = mem.readIntLittle(u32, key[20..24]);
-    k[6] = mem.readIntLittle(u32, key[24..28]);
-    k[7] = mem.readIntLittle(u32, key[28..32]);
-
     c[0] = counter;
     c[1] = mem.readIntLittle(u32, nonce[0..4]);
     c[2] = mem.readIntLittle(u32, nonce[4..8]);
     c[3] = mem.readIntLittle(u32, nonce[8..12]);
-    chaCha20_internal(out, in, k, c);
+    chaCha20_internal(out, in, keyToWords(key), c);
 }
 
 /// This is the original ChaCha20 before RFC 7539, which recommends using the
@@ -143,18 +152,8 @@ pub fn chaCha20With64BitNonce(out: []u8, in: []const u8, counter: u64, key: [32]
     assert(counter +% (in.len >> 6) >= counter);
 
     var cursor: usize = 0;
-    var k: [8]u32 = undefined;
+    const k = keyToWords(key);
     var c: [4]u32 = undefined;
-
-    k[0] = mem.readIntLittle(u32, key[0..4]);
-    k[1] = mem.readIntLittle(u32, key[4..8]);
-    k[2] = mem.readIntLittle(u32, key[8..12]);
-    k[3] = mem.readIntLittle(u32, key[12..16]);
-    k[4] = mem.readIntLittle(u32, key[16..20]);
-    k[5] = mem.readIntLittle(u32, key[20..24]);
-    k[6] = mem.readIntLittle(u32, key[24..28]);
-    k[7] = mem.readIntLittle(u32, key[28..32]);
-
     c[0] = @truncate(u32, counter);
     c[1] = @truncate(u32, counter >> 32);
     c[2] = mem.readIntLittle(u32, nonce[0..4]);
@@ -437,15 +436,15 @@ test "crypto.chacha20 test vector 5" {
 
 pub const chacha20poly1305_tag_size = 16;
 
-pub fn chacha20poly1305Seal(dst: []u8, plaintext: []const u8, data: []const u8, key: [32]u8, nonce: [12]u8) void {
-    assert(dst.len >= plaintext.len + chacha20poly1305_tag_size);
+pub fn chacha20poly1305SealDetached(ciphertext: []u8, tag: *[chacha20poly1305_tag_size]u8, plaintext: []const u8, data: []const u8, key: [32]u8, nonce: [12]u8) void {
+    assert(ciphertext.len >= plaintext.len);
 
     // derive poly1305 key
     var polyKey = [_]u8{0} ** 32;
     chaCha20IETF(polyKey[0..], polyKey[0..], 0, key, nonce);
 
     // encrypt plaintext
-    chaCha20IETF(dst[0..plaintext.len], plaintext, 1, key, nonce);
+    chaCha20IETF(ciphertext[0..plaintext.len], plaintext, 1, key, nonce);
 
     // construct mac
     var mac = Poly1305.init(polyKey[0..]);
@@ -455,7 +454,7 @@ pub fn chacha20poly1305Seal(dst: []u8, plaintext: []const u8, data: []const u8,
         const padding = 16 - (data.len % 16);
         mac.update(zeros[0..padding]);
     }
-    mac.update(dst[0..plaintext.len]);
+    mac.update(ciphertext[0..plaintext.len]);
     if (plaintext.len % 16 != 0) {
         const zeros = [_]u8{0} ** 16;
         const padding = 16 - (plaintext.len % 16);
@@ -465,19 +464,17 @@ pub fn chacha20poly1305Seal(dst: []u8, plaintext: []const u8, data: []const u8,
     mem.writeIntLittle(u64, lens[0..8], data.len);
     mem.writeIntLittle(u64, lens[8..16], plaintext.len);
     mac.update(lens[0..]);
-    mac.final(dst[plaintext.len..]);
+    mac.final(tag);
 }
 
-/// Verifies and decrypts an authenticated message produced by chacha20poly1305Seal.
-pub fn chacha20poly1305Open(dst: []u8, msgAndTag: []const u8, data: []const u8, key: [32]u8, nonce: [12]u8) !void {
-    if (msgAndTag.len < chacha20poly1305_tag_size) {
-        return error.InvalidMessage;
-    }
+pub fn chacha20poly1305Seal(ciphertextAndTag: []u8, plaintext: []const u8, data: []const u8, key: [32]u8, nonce: [12]u8) void {
+    return chacha20poly1305SealDetached(ciphertextAndTag[0..plaintext.len], ciphertextAndTag[plaintext.len..][0..chacha20poly1305_tag_size], plaintext, data, key, nonce);
+}
 
+/// Verifies and decrypts an authenticated message produced by chacha20poly1305SealDetached.
+pub fn chacha20poly1305OpenDetached(dst: []u8, ciphertext: []const u8, tag: *const [chacha20poly1305_tag_size]u8, data: []const u8, key: [32]u8, nonce: [12]u8) !void {
     // split ciphertext and tag
-    assert(dst.len >= msgAndTag.len - chacha20poly1305_tag_size);
-    var ciphertext = msgAndTag[0 .. msgAndTag.len - chacha20poly1305_tag_size];
-    var polyTag = msgAndTag[ciphertext.len..];
+    assert(dst.len >= ciphertext.len);
 
     // derive poly1305 key
     var polyKey = [_]u8{0} ** 32;
@@ -510,7 +507,7 @@ pub fn chacha20poly1305Open(dst: []u8, msgAndTag: []const u8, data: []const u8,
     // See https://github.com/ziglang/zig/issues/1776
     var acc: u8 = 0;
     for (computedTag) |_, i| {
-        acc |= (computedTag[i] ^ polyTag[i]);
+        acc |= (computedTag[i] ^ tag.*[i]);
     }
     if (acc != 0) {
         return error.AuthenticationFailed;
@@ -520,6 +517,75 @@ pub fn chacha20poly1305Open(dst: []u8, msgAndTag: []const u8, data: []const u8,
     chaCha20IETF(dst[0..ciphertext.len], ciphertext, 1, key, nonce);
 }
 
+/// Verifies and decrypts an authenticated message produced by chacha20poly1305Seal.
+pub fn chacha20poly1305Open(dst: []u8, ciphertextAndTag: []const u8, data: []const u8, key: [32]u8, nonce: [12]u8) !void {
+    if (ciphertextAndTag.len < chacha20poly1305_tag_size) {
+        return error.InvalidMessage;
+    }
+    const ciphertextLen = ciphertextAndTag.len - chacha20poly1305_tag_size;
+    return try chacha20poly1305OpenDetached(dst, ciphertextAndTag[0..ciphertextLen], ciphertextAndTag[ciphertextLen..][0..chacha20poly1305_tag_size], data, key, nonce);
+}
+
+fn hchacha20(input: [16]u8, key: [32]u8) [32]u8 {
+    var c: [4]u32 = undefined;
+    for (c) |_, i| {
+        c[i] = mem.readIntLittle(u32, input[4 * i ..][0..4]);
+    }
+    const ctx = initContext(keyToWords(key), c);
+    var x: [16]u32 = undefined;
+    chacha20Core(x[0..], ctx);
+    var out: [32]u8 = undefined;
+    mem.writeIntLittle(u32, out[0..4], x[0]);
+    mem.writeIntLittle(u32, out[4..8], x[1]);
+    mem.writeIntLittle(u32, out[8..12], x[2]);
+    mem.writeIntLittle(u32, out[12..16], x[3]);
+    mem.writeIntLittle(u32, out[16..20], x[12]);
+    mem.writeIntLittle(u32, out[20..24], x[13]);
+    mem.writeIntLittle(u32, out[24..28], x[14]);
+    mem.writeIntLittle(u32, out[28..32], x[15]);
+
+    return out;
+}
+
+fn extend(key: [32]u8, nonce: [24]u8) struct { key: [32]u8, nonce: [12]u8 } {
+    var subnonce: [12]u8 = undefined;
+    mem.set(u8, subnonce[0..4], 0);
+    mem.copy(u8, subnonce[4..], nonce[16..24]);
+    return .{
+        .key = hchacha20(nonce[0..16].*, key),
+        .nonce = subnonce,
+    };
+}
+
+pub fn xChaCha20IETF(out: []u8, in: []const u8, counter: u32, key: [32]u8, nonce: [24]u8) void {
+    const extended = extend(key, nonce);
+    chaCha20IETF(out, in, counter, extended.key, extended.nonce);
+}
+
+pub const xchacha20poly1305_tag_size = 16;
+
+pub fn xchacha20poly1305SealDetached(ciphertext: []u8, tag: *[chacha20poly1305_tag_size]u8, plaintext: []const u8, data: []const u8, key: [32]u8, nonce: [24]u8) void {
+    const extended = extend(key, nonce);
+    return chacha20poly1305SealDetached(ciphertext, tag, plaintext, data, extended.key, extended.nonce);
+}
+
+pub fn xchacha20poly1305Seal(ciphertextAndTag: []u8, plaintext: []const u8, data: []const u8, key: [32]u8, nonce: [24]u8) void {
+    const extended = extend(key, nonce);
+    return chacha20poly1305Seal(ciphertextAndTag, plaintext, data, extended.key, extended.nonce);
+}
+
+/// Verifies and decrypts an authenticated message produced by xchacha20poly1305SealDetached.
+pub fn xchacha20poly1305OpenDetached(plaintext: []u8, ciphertext: []const u8, tag: *const [chacha20poly1305_tag_size]u8, data: []const u8, key: [32]u8, nonce: [24]u8) !void {
+    const extended = extend(key, nonce);
+    return try chacha20poly1305OpenDetached(plaintext, ciphertext, tag, data, extended.key, extended.nonce);
+}
+
+/// Verifies and decrypts an authenticated message produced by xchacha20poly1305Seal.
+pub fn xchacha20poly1305Open(ciphertextAndTag: []u8, msgAndTag: []const u8, data: []const u8, key: [32]u8, nonce: [24]u8) !void {
+    const extended = extend(key, nonce);
+    return try chacha20poly1305Open(ciphertextAndTag, msgAndTag, data, extended.key, extended.nonce);
+}
+
 test "seal" {
     {
         const plaintext = "";
@@ -636,3 +702,105 @@ test "open" {
         testing.expectError(error.InvalidMessage, chacha20poly1305Open(out[0..], "", data[0..], key, bad_nonce));
     }
 }
+
+test "crypto.xchacha20" {
+    const key = [_]u8{69} ** 32;
+    const nonce = [_]u8{42} ** 24;
+    const input = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
+    {
+        var ciphertext: [input.len]u8 = undefined;
+        xChaCha20IETF(ciphertext[0..], input[0..], 0, key, nonce);
+        var buf: [2 * ciphertext.len]u8 = undefined;
+        testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{X}", .{ciphertext}), "E0A1BCF939654AFDBDC1746EC49832647C19D891F0D1A81FC0C1703B4514BDEA584B512F6908C2C5E9DD18D5CBC1805DE5803FE3B9CA5F193FB8359E91FAB0C3BB40309A292EB1CF49685C65C4A3ADF4F11DB0CD2B6B67FBC174BC2E860E8F769FD3565BBFAD1C845E05A0FED9BE167C240D");
+    }
+    {
+        const data = "Additional data";
+        var ciphertext: [input.len + xchacha20poly1305_tag_size]u8 = undefined;
+        xchacha20poly1305Seal(ciphertext[0..], input, data, key, nonce);
+        var out: [input.len]u8 = undefined;
+        try xchacha20poly1305Open(out[0..], ciphertext[0..], data, key, nonce);
+        var buf: [2 * ciphertext.len]u8 = undefined;
+        testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{X}", .{ciphertext}), "994D2DD32333F48E53650C02C7A2ABB8E018B0836D7175AEC779F52E961780768F815C58F1AA52D211498DB89B9216763F569C9433A6BBFCEFB4D4A49387A4C5207FBB3B5A92B5941294DF30588C6740D39DC16FA1F0E634F7246CF7CDCB978E44347D89381B7A74EB7084F754B90BDE9AAF5A94B8F2A85EFD0B50692AE2D425E234");
+        testing.expectEqualSlices(u8, out[0..], input);
+        ciphertext[0] += 1;
+        testing.expectError(error.AuthenticationFailed, xchacha20poly1305Open(out[0..], ciphertext[0..], data, key, nonce));
+    }
+}
+
+pub const Chacha20Poly1305 = struct {
+    pub const tag_length = 16;
+    pub const nonce_length = 12;
+    pub const key_length = 32;
+
+    /// c: ciphertext: output buffer should be of size m.len
+    /// at: authentication tag: output MAC
+    /// m: message
+    /// ad: Associated Data
+    /// npub: public nonce
+    /// k: private key
+    pub fn encrypt(c: []u8, at: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) void {
+        assert(c.len == m.len);
+        return chacha20poly1305SealDetached(c, at, m, ad, k, npub);
+    }
+
+    /// m: message: output buffer should be of size c.len
+    /// c: ciphertext
+    /// at: authentication tag
+    /// ad: Associated Data
+    /// npub: public nonce
+    /// k: private key
+    /// NOTE: the check of the authentication tag is currently not done in constant time
+    pub fn decrypt(m: []u8, c: []const u8, at: [tag_length]u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) !void {
+        assert(c.len == m.len);
+        return try chacha20poly1305OpenDetached(m, c, at[0..], ad, k, npub);
+    }
+};
+
+pub const XChacha20Poly1305 = struct {
+    pub const tag_length = 16;
+    pub const nonce_length = 24;
+    pub const key_length = 32;
+
+    /// c: ciphertext: output buffer should be of size m.len
+    /// at: authentication tag: output MAC
+    /// m: message
+    /// ad: Associated Data
+    /// npub: public nonce
+    /// k: private key
+    pub fn encrypt(c: []u8, at: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) void {
+        assert(c.len == m.len);
+        return xchacha20poly1305SealDetached(c, at, m, ad, k, npub);
+    }
+
+    /// m: message: output buffer should be of size c.len
+    /// c: ciphertext
+    /// at: authentication tag
+    /// ad: Associated Data
+    /// npub: public nonce
+    /// k: private key
+    /// NOTE: the check of the authentication tag is currently not done in constant time
+    pub fn decrypt(m: []u8, c: []const u8, at: [tag_length]u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) !void {
+        assert(c.len == m.len);
+        return try xchacha20poly1305OpenDetached(m, c, at[0..], ad, k, npub);
+    }
+};
+
+test "chacha20 AEAD API" {
+    const aeads = [_]type{ Chacha20Poly1305, XChacha20Poly1305 };
+    const input = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
+    const data = "Additional data";
+
+    inline for (aeads) |aead| {
+        const key = [_]u8{69} ** aead.key_length;
+        const nonce = [_]u8{42} ** aead.nonce_length;
+        var ciphertext: [input.len]u8 = undefined;
+        var tag: [aead.tag_length]u8 = undefined;
+        var out: [input.len]u8 = undefined;
+
+        aead.encrypt(ciphertext[0..], tag[0..], input, data, nonce, key);
+        try aead.decrypt(out[0..], ciphertext[0..], tag, data[0..], nonce, key);
+        testing.expectEqualSlices(u8, out[0..], input);
+        ciphertext[0] += 1;
+        testing.expectError(error.AuthenticationFailed, aead.decrypt(out[0..], ciphertext[0..], tag, data[0..], nonce, key));
+    }
+}