diff --git a/crypto_secretstream.js b/crypto_secretstream.js new file mode 100644 index 0000000..99e70a7 --- /dev/null +++ b/crypto_secretstream.js @@ -0,0 +1,271 @@ +/* eslint-disable camelcase */ +const assert = require('nanoassert') +const { randombytes_buf } = require('./randombytes') +const { + crypto_stream_chacha20_ietf, + crypto_stream_chacha20_ietf_xor, + crypto_stream_chacha20_ietf_xor_ic, + crypto_stream_chacha20_ietf_KEYBYTES +} = require('./crypto_stream_chacha20') +const { crypto_core_hchacha20, crypto_core_hchacha20_INPUTBYTES } = require('./internal/hchacha20') +const Poly1305 = require('./internal/poly1305') +const { sodium_increment, sodium_is_zero, sodium_memcmp } = require('./helpers') + +const crypto_onetimeauth_poly1305_BYTES = 16 +const crypto_secretstream_xchacha20poly1305_COUNTERBYTES = 4 +const crypto_secretstream_xchacha20poly1305_INONCEBYTES = 8 +const crypto_aead_xchacha20poly1305_ietf_KEYBYTES = 32 +const crypto_secretstream_xchacha20poly1305_KEYBYTES = crypto_aead_xchacha20poly1305_ietf_KEYBYTES +const crypto_aead_xchacha20poly1305_ietf_NPUBBYTES = 24 +const crypto_secretstream_xchacha20poly1305_HEADERBYTES = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES +const crypto_aead_xchacha20poly1305_ietf_ABYTES = 16 +const crypto_secretstream_xchacha20poly1305_ABYTES = 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES +const crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX = Number.MAX_SAFE_INTEGER +const crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX = Number.MAX_SAFE_INTEGER +const crypto_secretstream_xchacha20poly1305_TAGBYTES = 1 +const crypto_secretstream_xchacha20poly1305_TAG_MESSAGE = new Uint8Array([0]) +const crypto_secretstream_xchacha20poly1305_TAG_PUSH = new Uint8Array([1]) +const crypto_secretstream_xchacha20poly1305_TAG_REKEY = new Uint8Array([2]) +const crypto_secretstream_xchacha20poly1305_TAG_FINAL = new Uint8Array([crypto_secretstream_xchacha20poly1305_TAG_PUSH | crypto_secretstream_xchacha20poly1305_TAG_REKEY]) +const crypto_secretstream_xchacha20poly1305_STATEBYTES = crypto_secretstream_xchacha20poly1305_KEYBYTES + + crypto_secretstream_xchacha20poly1305_INONCEBYTES + crypto_secretstream_xchacha20poly1305_COUNTERBYTES + 8 + +const KEY_OFFSET = 0 +const NONCE_OFFSET = crypto_secretstream_xchacha20poly1305_KEYBYTES +const PAD_OFFSET = NONCE_OFFSET + crypto_secretstream_xchacha20poly1305_INONCEBYTES + crypto_secretstream_xchacha20poly1305_COUNTERBYTES + +const _pad0 = new Uint8Array(16) + +function STORE64_LE (dest, int) { + let mul = 1 + let i = 0 + dest[0] = int & 0xFF + while (++i < 8 && (mul *= 0x100)) { + dest[i] = (int / mul) & 0xFF + } +} + +function crypto_secretstream_xchacha20poly1305_counter_reset (state) { + assert(state.byteLength === crypto_secretstream_xchacha20poly1305_STATEBYTES, + 'state is should be crypto_secretstream_xchacha20poly1305_STATEBYTES long') + + const nonce = state.subarray(NONCE_OFFSET, PAD_OFFSET) + for (let i = 0; i < crypto_secretstream_xchacha20poly1305_COUNTERBYTES; i++) { + nonce[i] = 0 + } + nonce[0] = 1 +} + +function crypto_secretstream_xchacha20poly1305_keygen (k) { + assert(k.length === crypto_secretstream_xchacha20poly1305_KEYBYTES) + randombytes_buf(k) +} + +function crypto_secretstream_xchacha20poly1305_init_push (state, out, key) { + assert(state.byteLength === crypto_secretstream_xchacha20poly1305_STATEBYTES, + 'state is should be crypto_secretstream_xchacha20poly1305_STATEBYTES long') + assert(out instanceof Uint8Array && out.length === crypto_secretstream_xchacha20poly1305_HEADERBYTES, 'out not byte array of length crypto_secretstream_xchacha20poly1305_HEADERBYTES') + assert(key instanceof Uint8Array && key.length === crypto_secretstream_xchacha20poly1305_KEYBYTES, 'key not byte array of length crypto_secretstream_xchacha20poly1305_KEYBYTES') + + const k = state.subarray(KEY_OFFSET, NONCE_OFFSET) + const nonce = state.subarray(NONCE_OFFSET, PAD_OFFSET) + const pad = state.subarray(PAD_OFFSET) + + randombytes_buf(out, crypto_secretstream_xchacha20poly1305_HEADERBYTES) + crypto_core_hchacha20(k, out, key, null) + crypto_secretstream_xchacha20poly1305_counter_reset(state) + for (let i = 0; i < crypto_secretstream_xchacha20poly1305_INONCEBYTES; i++) { + nonce[i + crypto_secretstream_xchacha20poly1305_COUNTERBYTES] = out[i + crypto_core_hchacha20_INPUTBYTES] + } + pad.fill(0) +} + +function crypto_secretstream_xchacha20poly1305_init_pull (state, _in, key) { + assert(state.byteLength === crypto_secretstream_xchacha20poly1305_STATEBYTES, + 'state is should be crypto_secretstream_xchacha20poly1305_STATEBYTES long') + assert(_in instanceof Uint8Array && _in.length === crypto_secretstream_xchacha20poly1305_HEADERBYTES, + '_in not byte array of length crypto_secretstream_xchacha20poly1305_HEADERBYTES') + assert(key instanceof Uint8Array && key.length === crypto_secretstream_xchacha20poly1305_KEYBYTES, + 'key not byte array of length crypto_secretstream_xchacha20poly1305_KEYBYTES') + + const k = state.subarray(KEY_OFFSET, NONCE_OFFSET) + const nonce = state.subarray(NONCE_OFFSET, PAD_OFFSET) + const pad = state.subarray(PAD_OFFSET) + + crypto_core_hchacha20(k, _in, key, null) + crypto_secretstream_xchacha20poly1305_counter_reset(state) + + for (let i = 0; i < crypto_secretstream_xchacha20poly1305_INONCEBYTES; i++) { + nonce[i + crypto_secretstream_xchacha20poly1305_COUNTERBYTES] = _in[i + crypto_core_hchacha20_INPUTBYTES] + } + pad.fill(0) +} + +function crypto_secretstream_xchacha20poly1305_rekey (state) { + assert(state.byteLength === crypto_secretstream_xchacha20poly1305_STATEBYTES, + 'state is should be crypto_secretstream_xchacha20poly1305_STATEBYTES long') + + const k = state.subarray(KEY_OFFSET, NONCE_OFFSET) + const nonce = state.subarray(NONCE_OFFSET, PAD_OFFSET) + + const new_key_and_inonce = new Uint8Array( + crypto_stream_chacha20_ietf_KEYBYTES + crypto_secretstream_xchacha20poly1305_INONCEBYTES) + let i + for (i = 0; i < crypto_stream_chacha20_ietf_KEYBYTES; i++) { + new_key_and_inonce[i] = k[i] + } + for (i = 0; i < crypto_secretstream_xchacha20poly1305_INONCEBYTES; i++) { + new_key_and_inonce[crypto_stream_chacha20_ietf_KEYBYTES + i] = + nonce[crypto_secretstream_xchacha20poly1305_COUNTERBYTES + i] + } + crypto_stream_chacha20_ietf_xor(new_key_and_inonce, new_key_and_inonce, nonce, k) + for (i = 0; i < crypto_stream_chacha20_ietf_KEYBYTES; i++) { + k[i] = new_key_and_inonce[i] + } + for (i = 0; i < crypto_secretstream_xchacha20poly1305_INONCEBYTES; i++) { + nonce[crypto_secretstream_xchacha20poly1305_COUNTERBYTES + i] = + new_key_and_inonce[crypto_stream_chacha20_ietf_KEYBYTES + i] + } + crypto_secretstream_xchacha20poly1305_counter_reset(state) +} + +function crypto_secretstream_xchacha20poly1305_push (state, out, m, ad, tag) { + assert(state.byteLength === crypto_secretstream_xchacha20poly1305_STATEBYTES, + 'state is should be crypto_secretstream_xchacha20poly1305_STATEBYTES long') + if (!ad) ad = new Uint8Array(0) + + const k = state.subarray(KEY_OFFSET, NONCE_OFFSET) + const nonce = state.subarray(NONCE_OFFSET, PAD_OFFSET) + + const block = new Uint8Array(64) + const slen = new Uint8Array(8) + + assert(crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX <= + crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX) + + crypto_stream_chacha20_ietf(block, nonce, k) + const poly = new Poly1305(block) + block.fill(0) + + poly.update(ad, 0, ad.byteLength) + poly.update(_pad0, 0, (0x10 - ad.byteLength) & 0xf) + + block[0] = tag[0] + crypto_stream_chacha20_ietf_xor_ic(block, block, nonce, 1, k) + + poly.update(block, 0, block.byteLength) + out[0] = block[0] + + const c = out.subarray(1, out.byteLength) + crypto_stream_chacha20_ietf_xor_ic(c, m, nonce, 2, k) + poly.update(c, 0, m.byteLength) + poly.update(_pad0, 0, (0x10 - block.byteLength + m.byteLength) & 0xf) + + STORE64_LE(slen, ad.byteLength) + poly.update(slen, 0, slen.byteLength) + STORE64_LE(slen, block.byteLength + m.byteLength) + poly.update(slen, 0, slen.byteLength) + + const mac = out.subarray(1 + m.byteLength, out.byteLength) + poly.finish(mac, 0) + + assert(crypto_onetimeauth_poly1305_BYTES >= + crypto_secretstream_xchacha20poly1305_INONCEBYTES) + xor_buf(nonce.subarray(crypto_secretstream_xchacha20poly1305_COUNTERBYTES, nonce.length), + mac, crypto_secretstream_xchacha20poly1305_INONCEBYTES) + sodium_increment(nonce) + + if ((tag[0] & crypto_secretstream_xchacha20poly1305_TAG_REKEY) !== 0 || + sodium_is_zero(nonce.subarray(0, crypto_secretstream_xchacha20poly1305_COUNTERBYTES))) { + crypto_secretstream_xchacha20poly1305_rekey(state) + } + + return crypto_secretstream_xchacha20poly1305_ABYTES + m.byteLength +} + +function crypto_secretstream_xchacha20poly1305_pull (state, m, tag, _in, ad) { + assert(state.byteLength === crypto_secretstream_xchacha20poly1305_STATEBYTES, + 'state is should be crypto_secretstream_xchacha20poly1305_STATEBYTES long') + if (!ad) ad = new Uint8Array(0) + + const k = state.subarray(KEY_OFFSET, NONCE_OFFSET) + const nonce = state.subarray(NONCE_OFFSET, PAD_OFFSET) + + const block = new Uint8Array(64) + const slen = new Uint8Array(8) + const mac = new Uint8Array(crypto_onetimeauth_poly1305_BYTES) + + assert(_in.byteLength >= crypto_secretstream_xchacha20poly1305_ABYTES, + 'ciphertext is too short.') + + const mlen = _in.byteLength - crypto_secretstream_xchacha20poly1305_ABYTES + crypto_stream_chacha20_ietf(block, nonce, k) + const poly = new Poly1305(block) + block.fill(0) // sodium_memzero(block, sizeof block); + + poly.update(ad, 0, ad.byteLength) + poly.update(_pad0, 0, (0x10 - ad.byteLength) & 0xf) + + block.fill(0) // memset(block, 0, sizeof block); + block[0] = _in[0] + crypto_stream_chacha20_ietf_xor_ic(block, block, nonce, 1, k) + + tag[0] = block[0] + block[0] = _in[0] + poly.update(block, 0, block.byteLength) + + const c = _in.subarray(1, _in.length) + poly.update(c, 0, mlen) + + poly.update(_pad0, 0, (0x10 - block.byteLength + mlen) & 0xf) + + STORE64_LE(slen, ad.byteLength) + poly.update(slen, 0, slen.byteLength) + STORE64_LE(slen, block.byteLength + m.byteLength) + poly.update(slen, 0, slen.byteLength) + + poly.finish(mac, 0) + const stored_mac = _in.subarray(1 + mlen, _in.length) + + if (!sodium_memcmp(mac, stored_mac)) { + mac.fill(0) + throw new Error('MAC could not be verified.') + } + + crypto_stream_chacha20_ietf_xor_ic(m, c.subarray(0, m.length), nonce, 2, k) + xor_buf(nonce.subarray(crypto_secretstream_xchacha20poly1305_COUNTERBYTES, nonce.length), + mac, crypto_secretstream_xchacha20poly1305_INONCEBYTES) + sodium_increment(nonce) + + if ((tag & crypto_secretstream_xchacha20poly1305_TAG_REKEY) !== 0 || + sodium_is_zero(nonce.subarray(0, crypto_secretstream_xchacha20poly1305_COUNTERBYTES))) { + crypto_secretstream_xchacha20poly1305_rekey(state) + } + + return mlen +} + +function xor_buf (out, _in, n) { + for (let i = 0; i < n; i++) { + out[i] ^= _in[i] + } +} + +module.exports = { + crypto_secretstream_xchacha20poly1305_keygen, + crypto_secretstream_xchacha20poly1305_init_push, + crypto_secretstream_xchacha20poly1305_init_pull, + crypto_secretstream_xchacha20poly1305_rekey, + crypto_secretstream_xchacha20poly1305_push, + crypto_secretstream_xchacha20poly1305_pull, + crypto_secretstream_xchacha20poly1305_STATEBYTES, + crypto_secretstream_xchacha20poly1305_ABYTES, + crypto_secretstream_xchacha20poly1305_HEADERBYTES, + crypto_secretstream_xchacha20poly1305_KEYBYTES, + crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX, + crypto_secretstream_xchacha20poly1305_TAGBYTES, + crypto_secretstream_xchacha20poly1305_TAG_MESSAGE, + crypto_secretstream_xchacha20poly1305_TAG_PUSH, + crypto_secretstream_xchacha20poly1305_TAG_REKEY, + crypto_secretstream_xchacha20poly1305_TAG_FINAL +} diff --git a/index.js b/index.js index 980fdd6..2f5a3f5 100644 --- a/index.js +++ b/index.js @@ -23,6 +23,7 @@ forward(require('./crypto_aead')) forward(require('./crypto_onetimeauth')) forward(require('./crypto_scalarmult')) forward(require('./crypto_secretbox')) +forward(require('./crypto_secretstream')) forward(require('./crypto_shorthash')) forward(require('./crypto_sign')) forward(require('./crypto_stream')) diff --git a/internal/hchacha20.js b/internal/hchacha20.js new file mode 100644 index 0000000..f687b7d --- /dev/null +++ b/internal/hchacha20.js @@ -0,0 +1,128 @@ +/* eslint-disable camelcase */ +const { sodium_malloc } = require('../memory') +const assert = require('nanoassert') + +if (new Uint16Array([1])[0] !== 1) throw new Error('Big endian architecture is not supported.') + +const crypto_core_hchacha20_OUTPUTBYTES = 32 +const crypto_core_hchacha20_INPUTBYTES = 16 +const crypto_core_hchacha20_KEYBYTES = 32 +const crypto_core_hchacha20_CONSTBYTES = 16 + +function ROTL32 (x, b) { + x &= 0xFFFFFFFF + b &= 0xFFFFFFFF + return (x << b) | (x >>> (32 - b)) +} + +function LOAD32_LE (src, offset) { + assert(src instanceof Uint8Array, 'src not byte array') + let w = src[offset] + w |= src[offset + 1] << 8 + w |= src[offset + 2] << 16 + w |= src[offset + 3] << 24 + return w +} + +function STORE32_LE (dest, int, offset) { + assert(dest instanceof Uint8Array, 'dest not byte array') + var mul = 1 + var i = 0 + dest[offset] = int & 0xFF // grab bottom byte + while (++i < 4 && (mul *= 0x100)) { + dest[offset + i] = (int / mul) & 0xFF + } +} + +function QUARTERROUND (l, A, B, C, D) { + l[A] += l[B] + l[D] = ROTL32(l[D] ^ l[A], 16) + l[C] += l[D] + l[B] = ROTL32(l[B] ^ l[C], 12) + l[A] += l[B] + l[D] = ROTL32(l[D] ^ l[A], 8) + l[C] += l[D] + l[B] = ROTL32(l[B] ^ l[C], 7) +} + +function crypto_core_hchacha20 (out, _in, k, c) { + assert(out instanceof Uint8Array && out.length === 32, 'out is not an array of 32 bytes') + assert(k instanceof Uint8Array && k.length === 32, 'k is not an array of 32 bytes') + assert(c === null || (c instanceof Uint8Array && c.length === 16), 'c is not null or an array of 16 bytes') + + let i = 0 + const x = new Uint32Array(16) + if (!c) { + x[0] = 0x61707865 + x[1] = 0x3320646E + x[2] = 0x79622D32 + x[3] = 0x6B206574 + } else { + x[0] = LOAD32_LE(c, 0) + x[1] = LOAD32_LE(c, 4) + x[2] = LOAD32_LE(c, 8) + x[3] = LOAD32_LE(c, 12) + } + x[4] = LOAD32_LE(k, 0) + x[5] = LOAD32_LE(k, 4) + x[6] = LOAD32_LE(k, 8) + x[7] = LOAD32_LE(k, 12) + x[8] = LOAD32_LE(k, 16) + x[9] = LOAD32_LE(k, 20) + x[10] = LOAD32_LE(k, 24) + x[11] = LOAD32_LE(k, 28) + x[12] = LOAD32_LE(_in, 0) + x[13] = LOAD32_LE(_in, 4) + x[14] = LOAD32_LE(_in, 8) + x[15] = LOAD32_LE(_in, 12) + + for (i = 0; i < 10; i++) { + QUARTERROUND(x, 0, 4, 8, 12) + QUARTERROUND(x, 1, 5, 9, 13) + QUARTERROUND(x, 2, 6, 10, 14) + QUARTERROUND(x, 3, 7, 11, 15) + QUARTERROUND(x, 0, 5, 10, 15) + QUARTERROUND(x, 1, 6, 11, 12) + QUARTERROUND(x, 2, 7, 8, 13) + QUARTERROUND(x, 3, 4, 9, 14) + } + + STORE32_LE(out, x[0], 0) + STORE32_LE(out, x[1], 4) + STORE32_LE(out, x[2], 8) + STORE32_LE(out, x[3], 12) + STORE32_LE(out, x[12], 16) + STORE32_LE(out, x[13], 20) + STORE32_LE(out, x[14], 24) + STORE32_LE(out, x[15], 28) + + return 0 +} + +function crypto_core_hchacha20_outputbytes () { + return crypto_core_hchacha20_OUTPUTBYTES +} + +function crypto_core_hchacha20_inputbytes () { + return crypto_core_hchacha20_INPUTBYTES +} + +function crypto_core_hchacha20_keybytes () { + return crypto_core_hchacha20_KEYBYTES +} + +function crypto_core_hchacha20_constbytes () { + return crypto_core_hchacha20_CONSTBYTES +} + +module.exports = { + crypto_core_hchacha20_INPUTBYTES, + LOAD32_LE, + STORE32_LE, + QUARTERROUND, + crypto_core_hchacha20, + crypto_core_hchacha20_outputbytes, + crypto_core_hchacha20_inputbytes, + crypto_core_hchacha20_keybytes, + crypto_core_hchacha20_constbytes +}