Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
# `decrypt/oaep`
## Purpose
RSAES-OAEP decryption per RFC 8017 §7.1.2 — counterpart to
`encrypt/oaep`. Modern default for RSA public-key encryption.
The defining concern of this module is the **Manger defence**:
every padding-format failure surfaces the SAME
`CRYPTO_RSA.DECRYPT_FAILED` error so a remote attacker cannot
mount the chosen-ciphertext attack Manger described against
naive OAEP implementations in 2001.
## Features
- Combined error path — leading-byte mismatch, `lHash` mismatch
(wrong label), missing `0x01` separator, non-zero byte in
`PS` before separator, integer ≥ `n`, ciphertext length ≠ `k`,
modulus too small for the chosen hash: every failure raises
the single `DECRYPT_FAILED` code.
- Constant-time `lHash` comparison via `constantTimeEqual` from
`@suaveplan/crypto-utils` (the comparison we DO have a
guarantee about — the rest of the unwrap is best-effort).
- RSA blinding wired in by default; `{ blinding: false }` opt-out
emits the `crypto_rsa.blinding_disabled_total{operation="decrypt"}`
counter.
- CRT-accelerated private operation.
- `crypto_rsa.decrypt_duration_ms{scheme="oaep",hash}` histogram.
## Usage: Basic Example
```ts
import { rsaDecryptOaep } from "@suaveplan/crypto-rsa/decrypt/oaep";
const plaintext = rsaDecryptOaep(privateKey, ciphertext, {
hash: "SHA-256",
});
```
## Usage: Advanced Example
OAEP-SHA-384 with explicit label — the matching encrypt MUST
have used the same hash, MGF hash, AND label or this throws
`DECRYPT_FAILED`:
```ts
import { rsaDecryptOaep } from "@suaveplan/crypto-rsa/decrypt/oaep";
const plaintext = rsaDecryptOaep(privateKey, ciphertext, {
hash: "SHA-384",
mgfHash: "SHA-384",
label: new TextEncoder().encode("session-2026-05-11"),
allowLegacy: false,
});
```
## API Reference
### `rsaDecryptOaep(key, ciphertext, options?): Uint8Array`
- `key: RsaPrivateKey`
- `ciphertext: Uint8Array` — must equal `k = ceil(bitLength / 8)`.
- `options.hash?: RsaHashAlgorithm` — defaults to `"SHA-256"`.
- `options.mgfHash?: RsaHashAlgorithm` — defaults to `hash`.
- `options.label?: Uint8Array` — defaults to empty.
- `options.rng?: Rng` — randomness source for blinding.
- `options.blinding?: boolean` — defaults to `true`.
- `options.allowLegacy?: boolean` — accept 1024-bit moduli.
- Returns the recovered plaintext.
- 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 makes future drift detectable in code review.
- `constantTimeEqual` from `@suaveplan/crypto-utils` covers the
`lHash` byte comparison; the surrounding unwrap loop is NOT
constant-time because JavaScript bigint and `Uint8Array.slice`
are not. This is the same practical limitation as PKCS1-v1_5
decryption — for FIPS-grade side-channel resistance, defer to
`crypto-browser` (WebCrypto) or `crypto-sodium`.
- The OAEP separator scan walks `db` from index `hLen` onward
and rejects on either a non-zero, non-0x01 byte before the
separator or no separator at all. This implements RFC 8017
§7.1.2 step 3.g exactly.