| ... | ... | @@ -0,0 +1,430 @@ |
| 1 | const std = @import("std"); |
| 2 | const crypto = std.crypto; |
| 3 | const debug = std.debug; |
| 4 | const math = std.math; |
| 5 | const mem = std.mem; |
| 6 | |
| 7 | const Poly1305 = crypto.onetimeauth.Poly1305; |
| 8 | const Blake2b = crypto.hash.blake2.Blake2b; |
| 9 | const X25519 = crypto.dh.X25519; |
| 10 | |
| 11 | const Salsa20NonVecImpl = struct { |
| 12 | const BlockVec = [16]u32; |
| 13 | |
| 14 | fn initContext(key: [8]u32, d: [4]u32) BlockVec { |
| 15 | const c = "expand 32-byte k"; |
| 16 | const constant_le = comptime [4]u32{ |
| 17 | mem.readIntLittle(u32, c[0..4]), |
| 18 | mem.readIntLittle(u32, c[4..8]), |
| 19 | mem.readIntLittle(u32, c[8..12]), |
| 20 | mem.readIntLittle(u32, c[12..16]), |
| 21 | }; |
| 22 | return BlockVec{ |
| 23 | constant_le[0], key[0], key[1], key[2], |
| 24 | key[3], constant_le[1], d[0], d[1], |
| 25 | d[2], d[3], constant_le[2], key[4], |
| 26 | key[5], key[6], key[7], constant_le[3], |
| 27 | }; |
| 28 | } |
| 29 | |
| 30 | const QuarterRound = struct { |
| 31 | a: usize, |
| 32 | b: usize, |
| 33 | c: usize, |
| 34 | d: u6, |
| 35 | }; |
| 36 | |
| 37 | inline fn Rp(comptime a: usize, comptime b: usize, comptime c: usize, comptime d: u6) QuarterRound { |
| 38 | return QuarterRound{ |
| 39 | .a = a, |
| 40 | .b = b, |
| 41 | .c = c, |
| 42 | .d = d, |
| 43 | }; |
| 44 | } |
| 45 | |
| 46 | inline fn salsa20Core(x: *BlockVec, input: BlockVec) void { |
| 47 | const arx_steps = comptime [_]QuarterRound{ |
| 48 | Rp(4, 0, 12, 7), Rp(8, 4, 0, 9), Rp(12, 8, 4, 13), Rp(0, 12, 8, 18), |
| 49 | Rp(9, 5, 1, 7), Rp(13, 9, 5, 9), Rp(1, 13, 9, 13), Rp(5, 1, 13, 18), |
| 50 | Rp(14, 10, 6, 7), Rp(2, 14, 10, 9), Rp(6, 2, 14, 13), Rp(10, 6, 2, 18), |
| 51 | Rp(3, 15, 11, 7), Rp(7, 3, 15, 9), Rp(11, 7, 3, 13), Rp(15, 11, 7, 18), |
| 52 | Rp(1, 0, 3, 7), Rp(2, 1, 0, 9), Rp(3, 2, 1, 13), Rp(0, 3, 2, 18), |
| 53 | Rp(6, 5, 4, 7), Rp(7, 6, 5, 9), Rp(4, 7, 6, 13), Rp(5, 4, 7, 18), |
| 54 | Rp(11, 10, 9, 7), Rp(8, 11, 10, 9), Rp(9, 8, 11, 13), Rp(10, 9, 8, 18), |
| 55 | Rp(12, 15, 14, 7), Rp(13, 12, 15, 9), Rp(14, 13, 12, 13), Rp(15, 14, 13, 18), |
| 56 | }; |
| 57 | x.* = input; |
| 58 | var j: usize = 0; |
| 59 | while (j < 20) : (j += 2) { |
| 60 | inline for (arx_steps) |r| { |
| 61 | x[r.a] ^= math.rotl(u32, x[r.b] +% x[r.c], r.d); |
| 62 | } |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | fn hashToBytes(out: *[64]u8, x: BlockVec) void { |
| 67 | for (x) |w, i| { |
| 68 | mem.writeIntLittle(u32, out[i * 4 ..][0..4], w); |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | fn contextFeedback(x: *BlockVec, ctx: BlockVec) void { |
| 73 | var i: usize = 0; |
| 74 | while (i < 16) : (i += 1) { |
| 75 | x[i] +%= ctx[i]; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | fn salsa20Internal(out: []u8, in: []const u8, key: [8]u32, d: [4]u32) void { |
| 80 | var ctx = initContext(key, d); |
| 81 | var x: BlockVec = undefined; |
| 82 | var buf: [64]u8 = undefined; |
| 83 | var i: usize = 0; |
| 84 | while (i + 64 <= in.len) : (i += 64) { |
| 85 | salsa20Core(x[0..], ctx); |
| 86 | contextFeedback(&x, ctx); |
| 87 | hashToBytes(buf[0..], x); |
| 88 | var xout = out[i..]; |
| 89 | const xin = in[i..]; |
| 90 | var j: usize = 0; |
| 91 | while (j < 64) : (j += 1) { |
| 92 | xout[j] = xin[j]; |
| 93 | } |
| 94 | j = 0; |
| 95 | while (j < 64) : (j += 1) { |
| 96 | xout[j] ^= buf[j]; |
| 97 | } |
| 98 | ctx[9] += @boolToInt(@addWithOverflow(u32, ctx[8], 1, &ctx[8])); |
| 99 | } |
| 100 | if (i < in.len) { |
| 101 | salsa20Core(x[0..], ctx); |
| 102 | contextFeedback(&x, ctx); |
| 103 | hashToBytes(buf[0..], x); |
| 104 | |
| 105 | var xout = out[i..]; |
| 106 | const xin = in[i..]; |
| 107 | var j: usize = 0; |
| 108 | while (j < in.len % 64) : (j += 1) { |
| 109 | xout[j] = xin[j] ^ buf[j]; |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | fn hsalsa20(input: [16]u8, key: [32]u8) [32]u8 { |
| 115 | var c: [4]u32 = undefined; |
| 116 | for (c) |_, i| { |
| 117 | c[i] = mem.readIntLittle(u32, input[4 * i ..][0..4]); |
| 118 | } |
| 119 | const ctx = initContext(keyToWords(key), c); |
| 120 | var x: BlockVec = undefined; |
| 121 | salsa20Core(x[0..], ctx); |
| 122 | var out: [32]u8 = undefined; |
| 123 | mem.writeIntLittle(u32, out[0..4], x[0]); |
| 124 | mem.writeIntLittle(u32, out[4..8], x[5]); |
| 125 | mem.writeIntLittle(u32, out[8..12], x[10]); |
| 126 | mem.writeIntLittle(u32, out[12..16], x[15]); |
| 127 | mem.writeIntLittle(u32, out[16..20], x[6]); |
| 128 | mem.writeIntLittle(u32, out[20..24], x[7]); |
| 129 | mem.writeIntLittle(u32, out[24..28], x[8]); |
| 130 | mem.writeIntLittle(u32, out[28..32], x[9]); |
| 131 | return out; |
| 132 | } |
| 133 | }; |
| 134 | |
| 135 | const Salsa20Impl = Salsa20NonVecImpl; |
| 136 | |
| 137 | fn keyToWords(key: [32]u8) [8]u32 { |
| 138 | var k: [8]u32 = undefined; |
| 139 | var i: usize = 0; |
| 140 | while (i < 8) : (i += 1) { |
| 141 | k[i] = mem.readIntLittle(u32, key[i * 4 ..][0..4]); |
| 142 | } |
| 143 | return k; |
| 144 | } |
| 145 | |
| 146 | fn extend(key: [32]u8, nonce: [24]u8) struct { key: [32]u8, nonce: [8]u8 } { |
| 147 | return .{ |
| 148 | .key = Salsa20Impl.hsalsa20(nonce[0..16].*, key), |
| 149 | .nonce = nonce[16..24].*, |
| 150 | }; |
| 151 | } |
| 152 | |
| 153 | /// The Salsa20 stream cipher. |
| 154 | pub const Salsa20 = struct { |
| 155 | /// Nonce length in bytes. |
| 156 | pub const nonce_length = 8; |
| 157 | /// Key length in bytes. |
| 158 | pub const key_length = 32; |
| 159 | |
| 160 | /// Add the output of the Salsa20 stream cipher to `in` and stores the result into `out`. |
| 161 | /// WARNING: This function doesn't provide authenticated encryption. |
| 162 | /// Using the AEAD or one of the `box` versions is usually preferred. |
| 163 | pub fn xor(out: []u8, in: []const u8, counter: u64, key: [key_length]u8, nonce: [nonce_length]u8) void { |
| 164 | debug.assert(in.len == out.len); |
| 165 | |
| 166 | var d: [4]u32 = undefined; |
| 167 | d[0] = mem.readIntLittle(u32, nonce[0..4]); |
| 168 | d[1] = mem.readIntLittle(u32, nonce[4..8]); |
| 169 | d[2] = @truncate(u32, counter); |
| 170 | d[3] = @truncate(u32, counter >> 32); |
| 171 | Salsa20Impl.salsa20Internal(out, in, keyToWords(key), d); |
| 172 | } |
| 173 | }; |
| 174 | |
| 175 | /// The XSalsa20 stream cipher. |
| 176 | pub const XSalsa20 = struct { |
| 177 | /// Nonce length in bytes. |
| 178 | pub const nonce_length = 24; |
| 179 | /// Key length in bytes. |
| 180 | pub const key_length = 32; |
| 181 | |
| 182 | /// Add the output of the XSalsa20 stream cipher to `in` and stores the result into `out`. |
| 183 | /// WARNING: This function doesn't provide authenticated encryption. |
| 184 | /// Using the AEAD or one of the `box` versions is usually preferred. |
| 185 | pub fn xor(out: []u8, in: []const u8, counter: u64, key: [key_length]u8, nonce: [nonce_length]u8) void { |
| 186 | const extended = extend(key, nonce); |
| 187 | Salsa20.xor(out, in, counter, extended.key, extended.nonce); |
| 188 | } |
| 189 | }; |
| 190 | |
| 191 | /// The XSalsa20 stream cipher, combined with the Poly1305 MAC |
| 192 | pub const XSalsa20Poly1305 = struct { |
| 193 | /// Authentication tag length in bytes. |
| 194 | pub const tag_length = Poly1305.mac_length; |
| 195 | /// Nonce length in bytes. |
| 196 | pub const nonce_length = XSalsa20.nonce_length; |
| 197 | /// Key length in bytes. |
| 198 | pub const key_length = XSalsa20.key_length; |
| 199 | |
| 200 | /// c: ciphertext: output buffer should be of size m.len |
| 201 | /// tag: authentication tag: output MAC |
| 202 | /// m: message |
| 203 | /// ad: Associated Data |
| 204 | /// npub: public nonce |
| 205 | /// k: private key |
| 206 | pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) void { |
| 207 | debug.assert(c.len == m.len); |
| 208 | const extended = extend(k, npub); |
| 209 | var block0 = [_]u8{0} ** 64; |
| 210 | const mlen0 = math.min(32, m.len); |
| 211 | mem.copy(u8, block0[32..][0..mlen0], m[0..mlen0]); |
| 212 | Salsa20.xor(block0[0..], block0[0..], 0, extended.key, extended.nonce); |
| 213 | mem.copy(u8, c[0..mlen0], block0[32..][0..mlen0]); |
| 214 | Salsa20.xor(c[mlen0..], m[mlen0..], 1, extended.key, extended.nonce); |
| 215 | var mac = Poly1305.init(block0[0..32]); |
| 216 | mac.update(ad); |
| 217 | mac.update(c); |
| 218 | mac.final(tag); |
| 219 | } |
| 220 | |
| 221 | /// m: message: output buffer should be of size c.len |
| 222 | /// c: ciphertext |
| 223 | /// tag: authentication tag |
| 224 | /// ad: Associated Data |
| 225 | /// npub: public nonce |
| 226 | /// k: private key |
| 227 | pub fn decrypt(m: []u8, c: []const u8, tag: [tag_length]u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) !void { |
| 228 | debug.assert(c.len == m.len); |
| 229 | const extended = extend(k, npub); |
| 230 | var block0 = [_]u8{0} ** 64; |
| 231 | const mlen0 = math.min(32, c.len); |
| 232 | mem.copy(u8, block0[32..][0..mlen0], c[0..mlen0]); |
| 233 | Salsa20.xor(block0[0..], block0[0..], 0, extended.key, extended.nonce); |
| 234 | var mac = Poly1305.init(block0[0..32]); |
| 235 | mac.update(ad); |
| 236 | mac.update(c); |
| 237 | var computedTag: [tag_length]u8 = undefined; |
| 238 | mac.final(&computedTag); |
| 239 | var acc: u8 = 0; |
| 240 | for (computedTag) |_, i| { |
| 241 | acc |= (computedTag[i] ^ tag[i]); |
| 242 | } |
| 243 | if (acc != 0) { |
| 244 | mem.secureZero(u8, &computedTag); |
| 245 | return error.AuthenticationFailed; |
| 246 | } |
| 247 | mem.copy(u8, m[0..mlen0], block0[32..][0..mlen0]); |
| 248 | Salsa20.xor(m[mlen0..], c[mlen0..], 1, extended.key, extended.nonce); |
| 249 | } |
| 250 | }; |
| 251 | |
| 252 | /// NaCl-compatible secretbox API. |
| 253 | /// |
| 254 | /// A secretbox contains both an encrypted message and an authentication tag to verify that it hasn't been tampered with. |
| 255 | /// A secret key shared by all the recipients must be already known in order to use this API. |
| 256 | /// |
| 257 | /// Nonces are 192-bit large and can safely be chosen with a random number generator. |
| 258 | pub const secretBox = struct { |
| 259 | /// Key length in bytes. |
| 260 | pub const key_length = XSalsa20Poly1305.key_length; |
| 261 | /// Nonce length in bytes. |
| 262 | pub const nonce_length = XSalsa20Poly1305.nonce_length; |
| 263 | /// Authentication tag length in bytes. |
| 264 | pub const tag_length = XSalsa20Poly1305.tag_length; |
| 265 | |
| 266 | /// Encrypt and authenticate `m` using a nonce `npub` and a key `k`. |
| 267 | /// `c` must be exactly `tag_length` longer than `m`, as it will store both the ciphertext and the authentication tag. |
| 268 | pub fn seal(c: []u8, m: []const u8, npub: [nonce_length]u8, k: [key_length]u8) void { |
| 269 | debug.assert(c.len == tag_length + m.len); |
| 270 | XSalsa20Poly1305.encrypt(c[tag_length..], c[0..tag_length], m, "", npub, k); |
| 271 | } |
| 272 | |
| 273 | /// Verify and decrypt `c` using a nonce `npub` and a key `k`. |
| 274 | /// `m` must be exactly `tag_length` smaller than `c`, as `c` includes an authentication tag in addition to the encrypted message. |
| 275 | pub fn open(m: []u8, c: []const u8, npub: [nonce_length]u8, k: [key_length]u8) !void { |
| 276 | if (c.len < tag_length) { |
| 277 | return error.AuthenticationFailed; |
| 278 | } |
| 279 | debug.assert(m.len == c.len - tag_length); |
| 280 | return XSalsa20Poly1305.decrypt(m, c[tag_length..], c[0..tag_length].*, "", npub, k); |
| 281 | } |
| 282 | }; |
| 283 | |
| 284 | /// NaCl-compatible box API. |
| 285 | /// |
| 286 | /// A secretbox contains both an encrypted message and an authentication tag to verify that it hasn't been tampered with. |
| 287 | /// This construction uses public-key cryptography. A shared secret doesn't have to be known in advance by both parties. |
| 288 | /// Instead, a message is encrypted using a sender's secret key and a recipient's public key, |
| 289 | /// and is decrypted using the recipient's secret key and the sender's public key. |
| 290 | /// |
| 291 | /// Nonces are 192-bit large and can safely be chosen with a random number generator. |
| 292 | pub const box = struct { |
| 293 | /// Public key length in bytes. |
| 294 | pub const public_length = X25519.public_length; |
| 295 | /// Secret key length in bytes. |
| 296 | pub const secret_length = X25519.secret_length; |
| 297 | /// Shared key length in bytes. |
| 298 | pub const shared_length = XSalsa20Poly1305.key_length; |
| 299 | /// Seed (for key pair creation) length in bytes. |
| 300 | pub const seed_length = X25519.seed_length; |
| 301 | /// Nonce length in bytes. |
| 302 | pub const nonce_length = XSalsa20Poly1305.nonce_length; |
| 303 | /// Authentication tag length in bytes. |
| 304 | pub const tag_length = XSalsa20Poly1305.tag_length; |
| 305 | |
| 306 | /// A key pair. |
| 307 | pub const KeyPair = X25519.KeyPair; |
| 308 | |
| 309 | /// Compute a secret suitable for `secretbox` given a recipent's public key and a sender's secret key. |
| 310 | pub fn createSharedSecret(public_key: [public_length]u8, secret_key: [secret_length]u8) ![shared_length]u8 { |
| 311 | var p: [32]u8 = undefined; |
| 312 | try X25519.scalarmult(&p, secret_key, public_key); |
| 313 | const zero = [_]u8{0} ** 16; |
| 314 | return Salsa20Impl.hsalsa20(zero, p); |
| 315 | } |
| 316 | |
| 317 | /// Encrypt and authenticate a message using a recipient's public key `public_key` and a sender's `secret_key`. |
| 318 | pub fn seal(c: []u8, m: []const u8, npub: [nonce_length]u8, public_key: [public_length]u8, secret_key: [secret_length]u8) !void { |
| 319 | const shared_key = try createSharedSecret(public_key, secret_key); |
| 320 | return secretBox.seal(c, m, npub, shared_key); |
| 321 | } |
| 322 | |
| 323 | /// Verify and decrypt a message using a recipient's secret key `public_key` and a sender's `public_key`. |
| 324 | pub fn open(m: []u8, c: []const u8, npub: [nonce_length]u8, public_key: [public_length]u8, secret_key: [secret_length]u8) !void { |
| 325 | const shared_key = try createSharedSecret(public_key, secret_key); |
| 326 | return secretBox.open(m, c, npub, shared_key); |
| 327 | } |
| 328 | }; |
| 329 | |
| 330 | /// libsodium-compatible sealed boxes |
| 331 | /// |
| 332 | /// Sealed boxes are designed to anonymously send messages to a recipient given their public key. |
| 333 | /// Only the recipient can decrypt these messages, using their private key. |
| 334 | /// While the recipient can verify the integrity of the message, it cannot verify the identity of the sender. |
| 335 | /// |
| 336 | /// A message is encrypted using an ephemeral key pair, whose secret part is destroyed right after the encryption process. |
| 337 | pub const sealedBox = struct { |
| 338 | pub const public_length = box.public_length; |
| 339 | pub const secret_length = box.secret_length; |
| 340 | pub const seed_length = box.seed_length; |
| 341 | pub const seal_length = box.public_length + box.tag_length; |
| 342 | |
| 343 | /// A key pair. |
| 344 | pub const KeyPair = box.KeyPair; |
| 345 | |
| 346 | fn createNonce(pk1: [public_length]u8, pk2: [public_length]u8) [box.nonce_length]u8 { |
| 347 | var hasher = Blake2b(box.nonce_length * 8).init(.{}); |
| 348 | hasher.update(&pk1); |
| 349 | hasher.update(&pk2); |
| 350 | var nonce: [box.nonce_length]u8 = undefined; |
| 351 | hasher.final(&nonce); |
| 352 | return nonce; |
| 353 | } |
| 354 | |
| 355 | /// Encrypt a message `m` for a recipient whose public key is `public_key`. |
| 356 | /// `c` must be `seal_length` bytes larger than `m`, so that the required metadata can be added. |
| 357 | pub fn seal(c: []u8, m: []const u8, public_key: [public_length]u8) !void { |
| 358 | debug.assert(c.len == m.len + seal_length); |
| 359 | var ekp = try KeyPair.create(null); |
| 360 | const nonce = createNonce(ekp.public_key, public_key); |
| 361 | mem.copy(u8, c[0..public_length], ekp.public_key[0..]); |
| 362 | try box.seal(c[box.public_length..], m, nonce, public_key, ekp.secret_key); |
| 363 | mem.secureZero(u8, ekp.secret_key[0..]); |
| 364 | } |
| 365 | |
| 366 | /// Decrypt a message using a key pair. |
| 367 | /// `m` must be exactly `seal_length` bytes smaller than `c`, as `c` also includes metadata. |
| 368 | pub fn open(m: []u8, c: []const u8, keypair: KeyPair) !void { |
| 369 | if (c.len < seal_length) { |
| 370 | return error.AuthenticationFailed; |
| 371 | } |
| 372 | const epk = c[0..public_length]; |
| 373 | const nonce = createNonce(epk.*, keypair.public_key); |
| 374 | return box.open(m, c[public_length..], nonce, epk.*, keypair.secret_key); |
| 375 | } |
| 376 | }; |
| 377 | |
| 378 | test "xsalsa20poly1305" { |
| 379 | var msg: [100]u8 = undefined; |
| 380 | var msg2: [msg.len]u8 = undefined; |
| 381 | var c: [msg.len]u8 = undefined; |
| 382 | var key: [XSalsa20Poly1305.key_length]u8 = undefined; |
| 383 | var nonce: [XSalsa20Poly1305.nonce_length]u8 = undefined; |
| 384 | var tag: [XSalsa20Poly1305.tag_length]u8 = undefined; |
| 385 | try crypto.randomBytes(&msg); |
| 386 | try crypto.randomBytes(&key); |
| 387 | try crypto.randomBytes(&nonce); |
| 388 | |
| 389 | XSalsa20Poly1305.encrypt(c[0..], &tag, msg[0..], "ad", nonce, key); |
| 390 | try XSalsa20Poly1305.decrypt(msg2[0..], c[0..], tag, "ad", nonce, key); |
| 391 | } |
| 392 | |
| 393 | test "xsalsa20poly1305 secretbox" { |
| 394 | var msg: [100]u8 = undefined; |
| 395 | var msg2: [msg.len]u8 = undefined; |
| 396 | var key: [XSalsa20Poly1305.key_length]u8 = undefined; |
| 397 | var nonce: [box.nonce_length]u8 = undefined; |
| 398 | var boxed: [msg.len + box.tag_length]u8 = undefined; |
| 399 | try crypto.randomBytes(&msg); |
| 400 | try crypto.randomBytes(&key); |
| 401 | try crypto.randomBytes(&nonce); |
| 402 | |
| 403 | secretBox.seal(boxed[0..], msg[0..], nonce, key); |
| 404 | try secretBox.open(msg2[0..], boxed[0..], nonce, key); |
| 405 | } |
| 406 | |
| 407 | test "xsalsa20poly1305 box" { |
| 408 | var msg: [100]u8 = undefined; |
| 409 | var msg2: [msg.len]u8 = undefined; |
| 410 | var nonce: [box.nonce_length]u8 = undefined; |
| 411 | var boxed: [msg.len + box.tag_length]u8 = undefined; |
| 412 | try crypto.randomBytes(&msg); |
| 413 | try crypto.randomBytes(&nonce); |
| 414 | |
| 415 | var kp1 = try box.KeyPair.create(null); |
| 416 | var kp2 = try box.KeyPair.create(null); |
| 417 | try box.seal(boxed[0..], msg[0..], nonce, kp1.public_key, kp2.secret_key); |
| 418 | try box.open(msg2[0..], boxed[0..], nonce, kp2.public_key, kp1.secret_key); |
| 419 | } |
| 420 | |
| 421 | test "xsalsa20poly1305 sealedbox" { |
| 422 | var msg: [100]u8 = undefined; |
| 423 | var msg2: [msg.len]u8 = undefined; |
| 424 | var boxed: [msg.len + sealedBox.seal_length]u8 = undefined; |
| 425 | try crypto.randomBytes(&msg); |
| 426 | |
| 427 | var kp = try box.KeyPair.create(null); |
| 428 | try sealedBox.seal(boxed[0..], msg[0..], kp.public_key); |
| 429 | try sealedBox.open(msg2[0..], boxed[0..], kp); |
| 430 | } |