Files
2026-05-27 23:47:53 +01:00

3.1 KiB

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

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:

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.