diff --git a/array.js b/array.js index 02b5389..30c6aff 100644 --- a/array.js +++ b/array.js @@ -164,3 +164,21 @@ export const uniqueBy = (arr, mapper) => { } return result } + +/** + * @template {ArrayLike} ARR + * @template {function(ARR extends ArrayLike ? T : never, number, ARR):any} MAPPER + * @param {ARR} arr + * @param {MAPPER} mapper + * @return {Array} + */ +export const map = (arr, mapper) => { + /** + * @type {Array} + */ + const res = Array(arr.length) + for (let i = 0; i < arr.length; i++) { + res[i] = mapper(/** @type {any} */ (arr[i]), i, /** @type {any} */ (arr)) + } + return /** @type {any} */ (res) +} diff --git a/buffer.js b/buffer.js index 3d50186..5d76ece 100644 --- a/buffer.js +++ b/buffer.js @@ -6,6 +6,8 @@ import * as string from './string.js' import * as env from './environment.js' +import * as array from './array.js' +import * as math from './math.js' import * as encoding from './encoding.js' import * as decoding from './decoding.js' @@ -81,6 +83,27 @@ export const toBase64 = env.isBrowser ? toBase64Browser : toBase64Node /* c8 ignore next */ export const fromBase64 = env.isBrowser ? fromBase64Browser : fromBase64Node +/** + * Base64 is always a more efficient choice. This exists for utility purposes only. + * + * @param {Uint8Array} buf + */ +export const toHexString = buf => array.map(buf, b => b.toString(16).padStart(2, '0')).join('') + +/** + * Note: This function expects that the hex doesn't start with 0x.. + * + * @param {string} hex + */ +export const fromHexString = hex => { + const hlen = hex.length + const buf = new Uint8Array(math.ceil(hlen / 2)) + for (let i = 0; i < hlen; i += 2) { + buf[buf.length - i / 2 - 1] = Number.parseInt(hex.slice(hlen - i - 2, hlen - i), 16) + } + return buf +} + /** * Copy the content of an Uint8Array view to a new ArrayBuffer. * diff --git a/buffer.test.js b/buffer.test.js index 8cbd5f5..e9458ec 100644 --- a/buffer.test.js +++ b/buffer.test.js @@ -7,7 +7,7 @@ import * as prng from './prng.js' */ export const testRepeatBase64Encoding = tc => { const gen = tc.prng - const barr = prng.uint8Array(gen, 100000) + const barr = prng.uint8Array(gen, prng.uint32(gen, 0, 47)) const copied = buffer.copyUint8Array(barr) const encoded = buffer.toBase64(barr) t.assert(encoded.constructor === String) @@ -23,7 +23,25 @@ export const testRepeatBase64Encoding = tc => { /** * @param {t.TestCase} tc */ -export const testAnyEncoding = tc => { +export const testRepeatHexEncoding = tc => { + const gen = tc.prng + const barr = prng.uint8Array(gen, prng.uint32(gen, 0, 47)) + const copied = buffer.copyUint8Array(barr) + const encoded = buffer.toHexString(barr) + t.assert(encoded.constructor === String) + const decoded = buffer.fromHexString(encoded) + t.assert(decoded.constructor === Uint8Array) + t.assert(decoded.byteLength === barr.byteLength) + for (let i = 0; i < barr.length; i++) { + t.assert(barr[i] === decoded[i]) + } + t.compare(copied, decoded) +} + +/** + * @param {t.TestCase} _tc + */ +export const testAnyEncoding = _tc => { const obj = { val: 1, arr: [1, 2], str: '409231dtrnä' } const res = buffer.decodeAny(buffer.encodeAny(obj)) t.compare(obj, res) diff --git a/hash/sha256.js b/hash/sha256.js new file mode 100644 index 0000000..59850c5 --- /dev/null +++ b/hash/sha256.js @@ -0,0 +1,180 @@ +/** + * @module sha256 + * Spec: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf + * Resources: + * - https://web.archive.org/web/20150315061807/http://csrc.nist.gov/groups/STM/cavp/documents/shs/sha256-384-512.pdf + */ + +import * as binary from '../binary.js' +import * as math from '../math.js' + +/** + * @param {Uint8Array} data + */ +export const hash = data => { + // Init working variables. + /** + * See 4.2.2: Constant for sha256 & sha224 + * These words represent the first thirty-two bits of the fractional parts of + * the cube roots of the first sixty-four prime numbers. In hex, these constant words are (from left to + * right) + */ + const K = new Uint32Array([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]) + /** + * See 5.3.3. Initial hash value. + * + * These words were obtained by taking the first thirty-two bits of the fractional parts of the + * square roots of the first eight prime numbers. + * + * @todo shouldn't be a global variable + */ + const H = new Uint32Array([ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + ]) + // "Message schedule" - a working variable + const W = new Uint32Array(64) + const M = new Uint32Array(16) // 512 bit - current "view" of the data + let i = 0 + let isPaddedWith1 = false + for (; i + 56 <= data.length;) { + // write data in big endianess + let j = 0 + for (; j < M.length && i + 3 < data.length; j++) { + M[j] = data[i++] << 24 | data[i++] << 16 | data[i++] << 8 | data[i++] + } + if (i % 64 !== 0) { // there is still room to write partial content and the ending bit. + M.fill(0, j) + isPaddedWith1 = true + while (i < data.length) { + M[j] |= data[i] << ((3 - (i % 4)) * 8) + i++ + } + M[j] |= binary.BIT8 << ((3 - (i % 4)) * 8) + } + updateHash(H, W, K, M) + } + // write rest of the data, including the padding (using msb endiannes) + let j = 0 + M.fill(0) + for (; i < data.length; j++) { + for (let ci = 3; ci >= 0 && i < data.length; ci--) { + M[j] |= data[i++] << (ci * 8) + } + } + // Write padding of the message. See 5.1.2. + if (!isPaddedWith1) { + M[j - (i % 4 === 0 ? 0 : 1)] |= binary.BIT8 << ((3 - (i % 4)) * 8) + } + // write length of message (size in bits) as 64 bit uint + // @todo test that this works correctly + M[14] = math.round(data.byteLength / binary.BIT29) + M[15] = data.byteLength * 8 + updateHash(H, W, K, M) + // correct H endianness and return a Uint8Array view + const dv = new DataView(H.buffer) + for (let i = 0; i < H.length; i++) { + dv.setUint32(i * 4, H[i], false) + } + // logState(H) + return new Uint8Array(H.buffer) +} + +/** + * @param {Uint32Array} H - @todo since this is manipulated, it should be lower case + * @param {Uint32Array} W + * @param {Uint32Array} K + * @param {Uint32Array} M + */ +const updateHash = (H, W, K, M) => { + // Step 1. Prepare message schedule + for (let t = 0; t < 16; t++) { + // @todo omit M, write to W directly in above function + W[t] = M[t] + } + for (let t = 16; t < 64; t++) { + W[t] = sigma1to256(W[t - 2]) + W[t - 7] + sigma0to256(W[t - 15]) + W[t - 16] + } + // Step 2 + let a = H[0] + let b = H[1] + let c = H[2] + let d = H[3] + let e = H[4] + let f = H[5] + let g = H[6] + let h = H[7] + // Step 3 + for (let t = 0; t < 64; t++) { + const T1 = (h + sum1to256(e) + ch(e, f, g) + K[t] + W[t]) >>> 0 + const T2 = (sum0to256(a) + maj(a, b, c)) >>> 0 + h = g + g = f + f = e + e = (d + T1) >>> 0 + d = c + c = b + b = a + a = (T1 + T2) >>> 0 + } + H[0] += a + H[1] += b + H[2] += c + H[3] += d + H[4] += e + H[5] += f + H[6] += g + H[7] += h +} + +/** + * @param {number} x + * @param {number} y + * @param {number} z + */ +const ch = (x, y, z) => (x & y) ^ (~x & z) + +/** + * @param {number} x + * @param {number} y + * @param {number} z + */ +const maj = (x, y, z) => (x & y) ^ (x & z) ^ (y & z) + +/** + * @param {number} w - a 32bit uint + * @param {number} shift + */ +const rotr = (w, shift) => (w >>> shift) | (w << (32 - shift)) + +/** + * Helper for SHA-224 & SHA-256. See 4.1.2. + * @param {number} x + */ +const sum0to256 = x => rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22) + +/** + * Helper for SHA-224 & SHA-256. See 4.1.2. + * @param {number} x + */ +const sum1to256 = x => rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25) + +/** + * Helper for SHA-224 & SHA-256. See 4.1.2. + * @param {number} x + */ +const sigma0to256 = x => rotr(x, 7) ^ rotr(x, 18) ^ x >>> 3 + +/** + * Helper for SHA-224 & SHA-256. See 4.1.2. + * @param {number} x + */ +const sigma1to256 = x => rotr(x, 17) ^ rotr(x, 19) ^ x >>> 10 diff --git a/hash/sha256.test.js b/hash/sha256.test.js new file mode 100644 index 0000000..5febe89 --- /dev/null +++ b/hash/sha256.test.js @@ -0,0 +1,79 @@ +import * as t from '../testing.js' +import * as sha256 from './sha256.js' +import * as buffer from '../buffer.js' +import * as string from '../string.js' +import * as prng from '../prng.js' +import * as webcrypto from 'lib0/webcrypto' +import * as promise from '../promise.js' + +/** + * @param {t.TestCase} _tc + */ +export const testSha256Basics = async _tc => { + /** + * @param {string | Uint8Array} data input data (buffer or hex encoded) + * @param {string} result Expected result (hex encoded) + */ + const test = async (data, result) => { + data = typeof data === 'string' ? buffer.fromHexString(data) : data + const res = sha256.hash(data) + const resHex = buffer.toHexString(res) + t.assert(resHex === result) + const resWebcrypto = new Uint8Array(await webcrypto.subtle.digest('SHA-256', data)) + const resWebcryptoHex = buffer.toHexString(resWebcrypto) + t.assert(resWebcryptoHex === result) + } + + await test(string.encodeUtf8('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'), '248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1') + await test(string.encodeUtf8('abc'), 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') + await test('', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') +} + +/** + * @param {t.TestCase} tc + */ +export const testRepeatSha256Hashing = async tc => { + const LEN = prng.bool(tc.prng) ? prng.uint32(tc.prng, 0, 512) : prng.uint32(tc.prng, 0, 3003030) + console.log(LEN) + const data = prng.uint8Array(tc.prng, LEN) + const hashedCustom = sha256.hash(data) + const hashedWebcrypto = new Uint8Array(await webcrypto.subtle.digest('SHA-256', data)) + t.compare(hashedCustom, hashedWebcrypto) +} + +/** + * @param {t.TestCase} _tc + */ +export const testBenchmarkSha256 = async _tc => { + const N = 10000 // 100k + const BS = 530 + /** + * @type {Array} + */ + const datas = [] + for (let i = 0; i < N; i++) { + const data = new Uint8Array(BS) + webcrypto.getRandomValues(data) + datas.push(data) + } + t.measureTime(`[webcrypto sequentially] Time to hash ${N} random values of size ${BS}`, async () => { + for (let i = 0; i < N; i++) { + await webcrypto.subtle.digest('SHA-256', datas[i]) + } + }) + t.measureTime(`[webcrypto concurrent] Time to hash ${N} random values of size ${BS}`, async () => { + /** + * @type {Array>} + */ + const ps = [] + for (let i = 0; i < N; i++) { + ps.push(webcrypto.subtle.digest('SHA-256', datas[i])) + } + await promise.all(ps) + }) + t.measureTime(`[lib0] Time to hash ${N} random values of size ${BS}`, () => { + for (let i = 0; i < N; i++) { + sha256.hash(datas[i]) + } + }) +} diff --git a/test.html b/test.html index 8e3ce04..74061e6 100644 --- a/test.html +++ b/test.html @@ -32,6 +32,7 @@ "lib0/crypto/aes-gcm": "./crypto/aes-gcm.js", "lib0/crypto/ecdsa": "./crypto/ecdsa.js", "lib0/crypto/rsa-oaep": "./crypto/rsa-oaep.js", + "lib0/hash/rabin": "./hash/rabin.js", "lib0/decoding.js": "./decoding.js", "lib0/dist/decoding.cjs": "./dist/decoding.cjs", "lib0/decoding": "./decoding.js", diff --git a/test.js b/test.js index 094b6d6..a5cea28 100644 --- a/test.js +++ b/test.js @@ -3,6 +3,7 @@ import * as array from './array.test.js' import * as broadcastchannel from './broadcastchannel.test.js' import * as crypto from './crypto.test.js' import * as rabin from './hash/rabin.test.js' +import * as sha256 from './hash/sha256.test.js' import * as logging from './logging.test.js' import * as string from './string.test.js' import * as encoding from './encoding.test.js' @@ -46,6 +47,7 @@ runTests({ broadcastchannel, crypto, rabin, + sha256, logging, string, encoding,