diff --git a/changelog/unreleased/1606.md b/changelog/unreleased/1606.md new file mode 100644 index 0000000000..b943146bfe --- /dev/null +++ b/changelog/unreleased/1606.md @@ -0,0 +1,2 @@ +- `crypto/sha2`: `Sha2` publishes `hashBytes` and `blockBytes`, so consumers + sizing byte buffers read them instead of converting bit lengths themselves diff --git a/fjs/crypto/hmac/module.f.mjs b/fjs/crypto/hmac/module.f.mjs index 69adfade3b..05d4c842c4 100644 --- a/fjs/crypto/hmac/module.f.mjs +++ b/fjs/crypto/hmac/module.f.mjs @@ -44,8 +44,8 @@ const iPad = vec8(0x36n) * that takes a message and computes the HMAC. */ export const hmac = hashFunc => { - const { blockLength } = hashFunc - const p = repeat(blockLength >> 3n) + const { blockLength, blockBytes } = hashFunc + const p = repeat(blockBytes) const ip = p(iPad) const op = p(oPad) const c = computeSync(hashFunc) diff --git a/fjs/crypto/sha2/module.f.mjs b/fjs/crypto/sha2/module.f.mjs index 5bd10f0eed..e08474495d 100644 --- a/fjs/crypto/sha2/module.f.mjs +++ b/fjs/crypto/sha2/module.f.mjs @@ -11,7 +11,7 @@ * @import { Base, Sha2, State, V16, V8 } from './types.ts' */ -import { mask } from '../../types/bigint/module.f.mjs' +import { divUp8, mask } from '../../types/bigint/module.f.mjs' import { vec, length, @@ -257,6 +257,8 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }) => { const sha2 = ({ append, end, chunkLength }, hash, hashLength) => ({ hashLength, blockLength: chunkLength, + hashBytes: divUp8(hashLength), + blockBytes: divUp8(chunkLength), init: { hash, len: 0n, diff --git a/fjs/crypto/sha2/proof.f.mjs b/fjs/crypto/sha2/proof.f.mjs index 7fed93d8e9..dd139971d4 100644 --- a/fjs/crypto/sha2/proof.f.mjs +++ b/fjs/crypto/sha2/proof.f.mjs @@ -9,6 +9,19 @@ import { assertEq } from '../../asserts/module.f.mjs' import { map } from '../../types/list/module.f.mjs' import { base32, base64, computeSync, sha224, sha256, sha384, sha512, sha512x224, sha512x256 } from './module.f.mjs' +/** + * Every SHA-2 length is a whole number of bytes, so the rounded-up byte count + * and the exact division agree — this pins both that identity and the values. + * + * @type {(sha2: Sha2) => (hashBytes: bigint, blockBytes: bigint) => void} + */ +const checkBytes = ({ hashLength, blockLength, hashBytes, blockBytes }) => (h, b) => { + assertEq(hashBytes, h) + assertEq(blockBytes, b) + assertEq(hashBytes, hashLength >> 3n) + assertEq(blockBytes, blockLength >> 3n) +} + /** @type {(sha2: Sha2) => (x: bigint) => void} */ const checkEmpty = ({ init, end, hashLength }) => x => { const result = end(init) @@ -71,6 +84,17 @@ export const proof = { sha512x256: () => checkEmpty(sha512x256)(0xc672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967an), sha512x224: () => checkEmpty(sha512x224)(0x6ed0dd02806fa89e25de060c19d3ac86cabb87d6a0ddd05c333b84f4n), }, + // The byte counts consumers read instead of converting bit lengths + // themselves. Asserted against the bit length each is derived from, not + // against a hand-written number, so the pairing is what is pinned. + byteLengths: [ + () => checkBytes(sha224)(28n, 64n), + () => checkBytes(sha256)(32n, 64n), + () => checkBytes(sha384)(48n, 128n), + () => checkBytes(sha512)(64n, 128n), + () => checkBytes(sha512x224)(28n, 128n), + () => checkBytes(sha512x256)(32n, 128n), + ], utf8: [ () => { const e = 0x730e109bd7a8a32b1cb9d9a09aa2325d2430587ddbc0c38bad911525n diff --git a/fjs/crypto/sha2/todo/byte-length-fields.md b/fjs/crypto/sha2/todo/byte-length-fields.md deleted file mode 100644 index cec1039f9d..0000000000 --- a/fjs/crypto/sha2/todo/byte-length-fields.md +++ /dev/null @@ -1,55 +0,0 @@ -## byte-length-fields. Consumers re-derive `Sha2` byte lengths from bit counts - -**Priority:** P4 -**Status:** open - -### Problem - -`Sha2` publishes only bit counts (`fjs/crypto/sha2/types.ts:58-59`: -`hashLength`, `blockLength`), so every consumer that needs bytes re-does the -conversion — with two different spellings: - -```js -// fjs/crypto/hmac/module.f.mjs:47-48 -const { blockLength } = hashFunc -const p = repeat(blockLength >> 3n) - -// fjs/crypto/sign/module.f.mjs:78 -const rep = repeat(divUp8(hf.hashLength)) -``` - -Same three-step idiom — take a bit length off a `Sha2`, convert to bytes, -`repeat` a `vec8` constant (`oPad`/`iPad` at `hmac:32,37`, `x01`/`x00` at -`sign:52-53`) — written twice, once as `>> 3n` and once as `divUp8` -(`fjs/types/bigint/module.f.mjs`). The reader has to work out per site whether -the two roundings agree (they do: SHA-2 lengths are byte-multiples), and a -third variant of "digest bits → scalar" arithmetic lives in -`fjs/crypto/pow/module.f.mjs:79-80`. - -### Proposal - -Compute the byte counts once where the record is built — `sha2(...)` -(`fjs/crypto/sha2/module.f.mjs:257`) — and publish them on the type: - -```ts -// fjs/crypto/sha2/types.ts -readonly hashLength: bigint // bits (unchanged) -readonly blockLength: bigint // bits (unchanged) -readonly hashBytes: bigint -readonly blockBytes: bigint -``` - -`hmac` and `sign` then read `blockBytes`/`hashBytes` instead of converting, -and the bits-vs-bytes decision has one owner. - -### Tasks - -- [ ] Add the byte fields to `Sha2` and `sha2(...)`; proof-cover them for all - four variants. -- [ ] `hmac`: `repeat(blockBytes)`; `sign`: `repeat(hashBytes)`. -- [ ] `npx tsc`, `fjs t`. - -### Related - -- `fjs/crypto/sign/todo/computek-digest-param.md` — adjacent `sign`/sha2 - interface cleanup. diff --git a/fjs/crypto/sha2/types.ts b/fjs/crypto/sha2/types.ts index f4b94adf30..c5986537c6 100644 --- a/fjs/crypto/sha2/types.ts +++ b/fjs/crypto/sha2/types.ts @@ -57,6 +57,17 @@ export type Base = { export type Sha2 = { readonly hashLength: bigint readonly blockLength: bigint + /** + * `hashLength` and `blockLength` in whole bytes, rounded up. Consumers + * that size a byte buffer read these instead of converting: `hmac` and + * `sign` each used to do it themselves, with two different spellings + * (`>> 3n` and `divUp8`), leaving a reader to work out per site whether + * the two roundings agree. They do for every SHA-2 variant, whose lengths + * are byte multiples — which is exactly why the decision belongs here + * once rather than at each call site. + */ + readonly hashBytes: bigint + readonly blockBytes: bigint readonly init: State readonly append: Fold readonly end: (state: State) => Vec diff --git a/fjs/crypto/sign/module.f.mjs b/fjs/crypto/sign/module.f.mjs index 7672019378..b0afe15c1c 100644 --- a/fjs/crypto/sign/module.f.mjs +++ b/fjs/crypto/sign/module.f.mjs @@ -12,7 +12,7 @@ */ import { assertNotNullish } from '../../asserts/module.f.mjs' -import { bitLength, divUp8, roundUp8 } from '../../types/bigint/module.f.mjs' +import { bitLength, roundUp8 } from '../../types/bigint/module.f.mjs' import { empty, length, msb, repeat, unpack, vec, vec8 } from '../../types/bit_vec/module.f.mjs' import { hmac } from '../hmac/module.f.mjs' import { computeSync } from '../sha2/module.f.mjs' @@ -75,7 +75,7 @@ export const computeK = // step and all subsequent steps, we use the same H function as the // one used in step 'a' to process the input message; this choice // will be discussed in more detail in Section 3.6. - const rep = repeat(divUp8(hf.hashLength)) + const rep = repeat(hf.hashBytes) const v0 = rep(x01) // c. Set: // K = 0x00 0x00 0x00 ... 0x00