Skip to content

Commit

Permalink
Implement working sha256 algorithm (unoptimized)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmonad committed Jul 11, 2023
1 parent c678a2c commit bdb5177
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 2 deletions.
18 changes: 18 additions & 0 deletions array.js
Expand Up @@ -164,3 +164,21 @@ export const uniqueBy = (arr, mapper) => {
}
return result
}

/**
* @template {ArrayLike<any>} ARR
* @template {function(ARR extends ArrayLike<infer T> ? T : never, number, ARR):any} MAPPER
* @param {ARR} arr
* @param {MAPPER} mapper
* @return {Array<MAPPER extends function(...any): infer M ? M : never>}
*/
export const map = (arr, mapper) => {
/**
* @type {Array<any>}
*/
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)
}
23 changes: 23 additions & 0 deletions buffer.js
Expand Up @@ -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'

Expand Down Expand Up @@ -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.
*
Expand Down
22 changes: 20 additions & 2 deletions buffer.test.js
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
180 changes: 180 additions & 0 deletions 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
79 changes: 79 additions & 0 deletions 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<Uint8Array>}
*/
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<Promise<any>>}
*/
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])
}
})
}
1 change: 1 addition & 0 deletions test.html
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions test.js
Expand Up @@ -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'
Expand Down Expand Up @@ -46,6 +47,7 @@ runTests({
broadcastchannel,
crypto,
rabin,
sha256,
logging,
string,
encoding,
Expand Down

0 comments on commit bdb5177

Please sign in to comment.