import { invert } from "@noble/curves/abstract/modular"; import type { Rng } from "@suaveplan/types"; import { CRYPTO_RSA_CODES, CryptoRsaError } from "../errors/errors.js"; import type { RsaPublicKey } from "../keys/keys.types.js"; import { modPow } from "../math/modpow.js"; /** * Output of `blindMessage`: the blinded message representative `m * r^e * mod n` ready to feed into `rsaPrivateOperationCrt`, plus the modular * inverse `r^-1 mod n` the caller must keep until the private operation * returns. */ export interface BlindResult { readonly mPrime: bigint; readonly rInverse: bigint; } /** * RSA blinding (RFC 8017 §5.1.2 informational note). Given a message * representative `m`, sample a uniform `r ∈ [2, n-1]` coprime with `n` * and return `m' = m * r^e mod n` together with `r^-1 mod n`. * * Wired into `rsaDecryptPkcs1v15` / `rsaDecryptOaep` (and the Stage 2 * sign paths) by default — the `{ blinding: false }` opt-out emits the * `crypto_rsa.blinding_disabled_total{operation}` counter so production * dashboards can flag any caller that disables the timing-side-channel * defence. * * The rejection-sampling loop terminates after `MAX_REJECTIONS` attempts * with `KEYGEN_FAILED` — for a well-formed public key with `n` coprime to * almost every value, this never fires in practice. */ const MAX_REJECTIONS = 1024; export function blindMessage( m: bigint, key: RsaPublicKey, rng: Rng ): BlindResult { const n = key.n; if (m < 0n || m >= n) { throw new CryptoRsaError( CRYPTO_RSA_CODES.INVALID_KEY, "error.crypto-rsa.blinding.message-out-of-range" ); } let attempts = 0; while (attempts < MAX_REJECTIONS) { attempts++; const r = sampleCoprimeUniform(n, rng); if (r === undefined) { continue; } // gcd(r, n) === 1 is enforced inside sampleCoprimeUniform, so // invert always succeeds. The defence-in-depth try/catch was // removed: no upstream input can drive `invert` to throw here // and a dead catch block undermines coverage as deliberate // dead code. const rInverse = invert(r, n); const rE = modPow(r, key.e, n); const mPrime = (m * rE) % n; return { mPrime, rInverse }; } throw new CryptoRsaError( CRYPTO_RSA_CODES.KEYGEN_FAILED, "error.crypto-rsa.blinding.exhausted" ); } /** * Reverse the blinding step: given the unblinded private-operation * output `s = (m')^d mod n`, return the true `m^d mod n` by multiplying * with `r^-1` modulo `n`. */ export function unblindResult(s: bigint, rInverse: bigint, n: bigint): bigint { return (s * rInverse) % n; } /** * Sample a uniform random `r ∈ [2, n-1]` coprime with `n`. Returns * `undefined` when the candidate happened to share a factor with `n` so * the caller can retry. Uses byte-wise rejection sampling (drop any * candidate `≥ n` AND any candidate sharing a factor) for an unbiased * distribution across `Z*_n`. */ function sampleCoprimeUniform(n: bigint, rng: Rng): bigint | undefined { const bitLen = n.toString(2).length; const byteLen = Math.ceil(bitLen / 8); const topMask = (1 << (bitLen - (byteLen - 1) * 8)) - 1; const bytes = nextBytesViaRng(rng, byteLen); bytes[0] = (bytes[0] as number) & topMask; let r = 0n; for (const b of bytes) { r = (r << 8n) | BigInt(b); } if (r < 2n || r >= n) { return undefined; } if (gcd(r, n) !== 1n) { return undefined; } return r; } function gcd(a: bigint, b: bigint): bigint { // Internal helper — only called with non-negative inputs from // sampleCoprimeUniform (where r ∈ [2, n-1] and n > 0). let x = a; let y = b; while (y !== 0n) { [x, y] = [y, x % y]; } return x; } /** * Pull `n` bytes from an `Rng`. Uses the optional `nextBytes` shortcut * when present (true for CSPRNG-backed `Rng` values) and falls back to * `nextUint32` byte-extraction otherwise (so deterministic seeded RNGs * used in tests work without implementing `nextBytes`). */ export function nextBytesViaRng(rng: Rng, n: number): Uint8Array { if (rng.nextBytes !== undefined) { const out = rng.nextBytes(n); if (out.length !== n) { throw new CryptoRsaError( CRYPTO_RSA_CODES.KEYGEN_FAILED, "error.crypto-rsa.blinding.rng-short-read" ); } return out; } const out = new Uint8Array(n); let i = 0; while (i < n) { const u32 = rng.nextUint32() >>> 0; out[i] = (u32 >>> 24) & 0xff; if (i + 1 < n) out[i + 1] = (u32 >>> 16) & 0xff; if (i + 2 < n) out[i + 2] = (u32 >>> 8) & 0xff; if (i + 3 < n) out[i + 3] = u32 & 0xff; i += 4; } return out; }