Files
skills/docs/security-stack/crypto/crypto-rsa/src/encrypt/oaep.md
2026-05-29 08:20:56 +01:00

3.2 KiB

encrypt/oaep

Purpose

RSAES-OAEP encryption per RFC 8017 §7.1.1. The Optimal Asymmetric Encryption Padding (OAEP) scheme is the modern default for RSA public-key encryption — provably secure under the random-oracle model when the underlying hash is collision-resistant, and immune to the chosen-ciphertext attacks (Bleichenbacher, Manger) that plague PKCS1-v1_5.

This module provides the encryption side; the matching decrypt lives in decrypt/oaep and applies the Manger-safe combined error path.

Features

  • RFC 8017 §7.1.1 reference algorithm with caller-overridable hash (SHA-256 default), MGF hash (defaults to the OAEP hash), and optional label (defaults to empty).
  • Routes hashing through @suaveplan/crypto-hash for SHA-256/384/512 and @noble/hashes/sha1 for SHA-1; same dispatcher Stage 2's mgf1 uses, so OAEP and MGF1 stay in lock-step.
  • Modulus-length policy gate runs first; sub-1024-bit keys rejected outright.
  • Plaintext oversize → CRYPTO_RSA.INVALID_KEY with { mLen, max } context for diagnosability.
  • crypto_rsa.encrypt_duration_ms{scheme="oaep",hash} histogram.

Usage: Basic Example

import { rsaEncryptOaep } from "@suaveplan/crypto-rsa/encrypt/oaep";

const ciphertext = rsaEncryptOaep(
    publicKey,
    new TextEncoder().encode("hello"),
    { hash: "SHA-256" }
);

Usage: Advanced Example

OAEP with SHA-384 hashing and a non-empty label (mirrors a common JWE RSA-OAEP-384 deployment with a context-bound label):

import { rsaEncryptOaep } from "@suaveplan/crypto-rsa/encrypt/oaep";

const label = new TextEncoder().encode("session-2026-05-11");
const ct = rsaEncryptOaep(publicKey, plaintext, {
    hash: "SHA-384",
    mgfHash: "SHA-384",
    label,
    allowLegacy: false,
});

API Reference

rsaEncryptOaep(key, plaintext, options?): Uint8Array

  • key: RsaPublicKey
  • plaintext: Uint8Array — at most k - 2*hLen - 2 bytes.
  • options.hash?: RsaHashAlgorithm — defaults to "SHA-256".
  • options.mgfHash?: RsaHashAlgorithm — defaults to hash.
  • options.label?: Uint8Array — defaults to empty.
  • options.rng?: Rng — defaults to defaultRng().
  • options.allowLegacy?: boolean — accept 1024-bit moduli.
  • Throws CRYPTO_RSA.INVALID_KEY on plaintext oversize, CRYPTO_RSA.UNSUPPORTED_HASH on unknown algorithm, CRYPTO_RSA.MGF_FAILURE on absurd mask-length input, modulus-length policy violations on bad key.

Implementation Notes

  • The seed is drawn from the injected Rng via the shared nextBytesViaRng helper (same path as blinding) so seeded test RNGs work transparently.
  • We share the mgf1 and digestWith implementations from Stage 2's src/encoding/mgf1.ts rather than duplicating — RFC 8017 §B.2.1 is short but having two copies invites drift.
  • db is allocated once and built in place; XOR with the mask produces a new buffer rather than mutating in place because xor from @suaveplan/crypto-utils returns a fresh allocation (length-mismatch guard).
  • The OAEP encoding allocates O(k) — for 4096-bit keys this is 512 bytes. Hot-loop callers (e.g. JWE bulk re-encryption) should prefer keeping a single RsaPublicKey and amortising the modpow cost over many ciphertexts.