# `decrypt/pkcs1v15` ## Purpose RSAES-PKCS1-v1_5 decryption per RFC 8017 §7.2.2. Counterpart to `encrypt/pkcs1v15`. Provides legacy interop for protocols that predate OAEP (TLS 1.0–1.2 RSA key transport, JWE `RSA1_5` legacy mode). Modern callers MUST prefer `decrypt/oaep`. The defining concern of this module is the **Bleichenbacher defence**: every padding-format failure surfaces the SAME `CRYPTO_RSA.DECRYPT_FAILED` error so a remote attacker observing the typed-error code (or the OTel error counter) cannot mount a million-message chosen-ciphertext attack. ## Features - Combined error path — wrong leading byte, wrong block-type marker, missing `0x00` separator, `PS` shorter than 8 bytes, ciphertext length ≠ `k`, post-decrypt integer ≥ `n`: every failure throws the single `DECRYPT_FAILED` code. - RSA blinding wired in by default — `{ blinding: false }` opt-out emits the `crypto_rsa.blinding_disabled_total{operation="decrypt"}` counter so production dashboards can flag any caller that disables the timing-side-channel defence. - CRT-accelerated private operation (`rsaPrivateOperationCrt`). - Modulus-length policy gate runs first. - `crypto_rsa.decrypt_duration_ms{scheme="pkcs1v15"}` histogram. ## Usage: Basic Example ```ts import { rsaDecryptPkcs1v15 } from "@suaveplan/crypto-rsa/decrypt/pkcs1v15"; import type { RsaPrivateKey } from "@suaveplan/crypto-rsa"; declare const privateKey: RsaPrivateKey; declare const ciphertext: Uint8Array; const plaintext = rsaDecryptPkcs1v15(privateKey, ciphertext, { allowLegacy: false, }); ``` ## Usage: Advanced Example Disable blinding (NOT recommended outside benchmarks) — the counter increment surfaces the choice in production telemetry: ```ts import { rsaDecryptPkcs1v15 } from "@suaveplan/crypto-rsa/decrypt/pkcs1v15"; const plaintext = rsaDecryptPkcs1v15(privateKey, ciphertext, { blinding: false, // emits crypto_rsa.blinding_disabled_total{operation="decrypt"} allowLegacy: true, }); ``` ## API Reference ### `rsaDecryptPkcs1v15(key, ciphertext, options?): Uint8Array` - `key: RsaPrivateKey` — modulus must satisfy the policy gate. - `ciphertext: Uint8Array` — must be exactly `k` bytes; any other length throws `DECRYPT_FAILED`. - `options.rng?: Rng` — randomness source for blinding. - `options.blinding?: boolean` — defaults to `true`. Set to `false` only for benchmark comparisons. - `options.allowLegacy?: boolean` — accept 1024-bit moduli. - Returns the recovered plaintext (`em.length - separator - 1` bytes). - Throws `CRYPTO_RSA.DECRYPT_FAILED` on every padding failure. ## Implementation Notes - The combined error path is enforced by routing every failure through the private `decryptFailed()` helper — a single callsite for `new CryptoRsaError(DECRYPT_FAILED, ...)` makes it impossible for a future edit to accidentally introduce a differentiated error. - True constant-time decryption is not achievable in JavaScript without WebCrypto (V8/JSC bigint ops are not constant-time). We mitigate with blinding and with eager-throw bucketing all padding failures under one error class. For FIPS-grade side-channel resistance, route through `crypto-browser` (WebCrypto) or `crypto-sodium`. - The `c >= key.n` check is RFC 8017 §5.1.2's "ciphertext representative out of range" condition — also bucketed under `DECRYPT_FAILED` so callers cannot distinguish "padding bad" from "ciphertext outside `Z*_n`".