Appearance
@pqc-sdk/core / pqc
Variable: pqc
constpqc:object
SDK entry point: post-quantum hybrid encryption and digital signatures with safe defaults, zero configuration.
keys.generate() with no arguments still returns ml-kem-768 (the pqcenc.v1 envelope); pass { algorithm: 'x-wing' } for the classical+PQC hybrid KEM (X25519 + ML-KEM-768, pqcenc.v2 envelope) recommended for long-term data — see the "Choosing an algorithm" guide. The no-arg default changes to x-wing at v1.0.
Type Declaration
decrypt
decrypt: (
ciphertext,secretKey) =>Promise<Uint8Array<ArrayBufferLike>>
Decrypts a ciphertext produced by encrypt, discriminating the envelope on its leading version byte (0x01 = ml-kem-768 v1, 0x02 = x-wing v2; unknown versions fail with INVALID_CIPHERTEXT). If the ciphertext was tampered with or the key does not match, it throws PqcError with code DECRYPTION_FAILED — it never returns corrupted data.
Parameters
ciphertext
Uint8Array
secretKey
Returns
Promise<Uint8Array<ArrayBufferLike>>
Example
ts
import { pqc } from '@pqc-sdk/core';
const plaintext = await pqc.decrypt(ciphertext, pair.secretKey);
new TextDecoder().decode(plaintext);decryptStream
decryptStream: (
secretKey,ciphertext) =>AsyncGenerator<Uint8Array<ArrayBufferLike>>
Decrypts a stream produced by encryptStream, discriminating the envelope on its leading version byte the same way decrypt does (0x03 = ml-kem-768 streaming, 0x04 = x-wing streaming; unrelated to and rejected the same way as the one-shot 0x01/0x02 bytes).
IMPORTANT — incremental release, read before writing decrypted output anywhere observable. This function yields each plaintext chunk as soon as that specific chunk authenticates. Every yielded chunk is genuinely authentic on its own — but that is not the same guarantee one-shot decrypt gives, where the call returning already means the whole plaintext is authentic. Here, a truncated or tampered stream can — and, for a stream truncated after the point of tampering, will — yield one or more genuine prefix chunks before the iterable throws PqcError (DECRYPTION_FAILED). The only signal that the full plaintext is authentic and complete is the iterable finishing without throwing — never any individual yielded chunk, and never "N chunks came out without error yet." A consumer writing this output to a file, a socket, or anywhere else observable must treat that output as provisional until the loop below completes cleanly, and must discard or roll back whatever was written if it throws instead. This is structural to streaming/online AEAD (age has the same property), not a shortcut this implementation took — see docs/serialization-format.md §9.3 for the full decode algorithm this follows.
Parameters
secretKey
ciphertext
AsyncIterable<Uint8Array<ArrayBufferLike>>
Returns
AsyncGenerator<Uint8Array<ArrayBufferLike>>
Example
ts
import { pqc } from '@pqc-sdk/core';
const pair = await pqc.keys.generate();
async function* source() {
yield new TextEncoder().encode('streamed data');
}
const ciphertextChunks: Uint8Array[] = [];
for await (const chunk of pqc.encryptStream(pair.publicKey, source())) {
ciphertextChunks.push(chunk);
}
async function* replay() {
yield* ciphertextChunks;
}
// Correct handling of the incremental-release property: buffer chunks
// as they arrive, but only treat them as real output once the loop
// finishes without throwing. On error, the provisional buffer is
// discarded — never returned, never written to a permanent sink.
const provisional: Uint8Array[] = [];
let plaintext: Uint8Array;
try {
for await (const chunk of pqc.decryptStream(pair.secretKey, replay())) {
provisional.push(chunk); // authentic on its own, but NOT proof of completeness
}
// Reached only on clean completion — now, and only now, the full
// plaintext is confirmed authentic.
plaintext = new Uint8Array(provisional.reduce((n, c) => n + c.length, 0));
let offset = 0;
for (const chunk of provisional) {
plaintext.set(chunk, offset);
offset += chunk.length;
}
} catch (error) {
// provisional is discarded here, not used — the stream never
// completed, so nothing in it is trustworthy as "the" plaintext.
throw error;
}decryptWebStream
decryptWebStream: (
secretKey) =>TransformStream<Uint8Array<ArrayBufferLike>,Uint8Array<ArrayBufferLike>>
decryptStream as a Web Streams TransformStream. Same incremental-release property as decryptStream applies here too — see its documentation. Piping into a TransformStream does not change that property: bytes written to a downstream sink before the pipe completes are provisional, and the sink must be treated as incomplete until pipeTo's returned promise resolves without rejecting.
Parameters
secretKey
Returns
TransformStream<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>
Example
ts
import { createReadStream, createWriteStream } from 'node:fs';
import { Readable, Writable } from 'node:stream';
import { pqc } from '@pqc-sdk/core';
// pipeTo's promise rejecting means the sink file holds an incomplete,
// unverified prefix — callers must delete/ignore it on rejection, never
// treat a partially-written output file as done.
await Readable.toWeb(createReadStream('large-file.bin.enc'))
.pipeThrough(pqc.decryptWebStream(pair.secretKey))
.pipeTo(Writable.toWeb(createWriteStream('large-file.bin')));encrypt
encrypt: (
data,publicKey) =>Promise<Uint8Array<ArrayBufferLike>>
Hybrid encryption: encapsulates a secret with the public key's KEM and encrypts the data with AES-256-GCM using that secret. The result is a single self-contained Uint8Array that only decrypt can open.
The key chooses the envelope (docs/serialization-format.md §2): an ml-kem-768 key (FIPS 203) produces a v1 envelope, an x-wing key (X25519 + ML-KEM-768 hybrid, draft-connolly-cfrg-xwing-kem-10) a v2 one.
Parameters
data
string | Uint8Array<ArrayBufferLike>
publicKey
Returns
Promise<Uint8Array<ArrayBufferLike>>
Example
ts
import { pqc } from '@pqc-sdk/core';
const pair = await pqc.keys.generate();
const ciphertext = await pqc.encrypt('sensitive data', pair.publicKey);
const hybrid = await pqc.keys.generate({ algorithm: 'x-wing' });
const v2Ciphertext = await pqc.encrypt('sensitive data', hybrid.publicKey);encryptStream
encryptStream: (
publicKey,plaintext,options?) =>AsyncGenerator<Uint8Array<ArrayBufferLike>>
Hybrid streaming encryption: like encrypt, but for payloads too large to hold in memory at once. Encapsulates one KEM secret per stream (fresh, single-use, same as encrypt) and seals fixed-size chunks of plaintext under it, using age's STREAM chunk framing (docs/serialization-format.md §9). Bounded memory: roughly one chunk at a time, independent of total payload size.
Yields the 3-byte header and KEM ciphertext first, then one sealed chunk per plaintext chunk. Concatenating everything yielded reproduces the full wire format §9 describes.
chunkSize (advanced, rarely needed) defaults to 64 KiB — see StreamOptions.
Parameters
publicKey
plaintext
AsyncIterable<Uint8Array<ArrayBufferLike>>
options?
Returns
AsyncGenerator<Uint8Array<ArrayBufferLike>>
Example
ts
import { pqc } from '@pqc-sdk/core';
const pair = await pqc.keys.generate();
async function* source() {
yield new TextEncoder().encode('streamed data');
}
const parts: Uint8Array[] = [];
for await (const chunk of pqc.encryptStream(pair.publicKey, source())) {
parts.push(chunk);
}encryptWebStream
encryptWebStream: (
publicKey,options?) =>TransformStream<Uint8Array<ArrayBufferLike>,Uint8Array<ArrayBufferLike>>
encryptStream as a Web Streams TransformStream, for pipeThrough/pipeTo pipelines on runtimes with the WHATWG Streams API (Node 18+, Deno, Cloudflare Workers — see docs/compatibility.md for which are actually verified). A thin wrapper: all the cryptographic work happens in encryptStream, this only bridges the two shapes.
Parameters
publicKey
options?
Returns
TransformStream<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>
Example
ts
import { createReadStream, createWriteStream } from 'node:fs';
import { Readable, Writable } from 'node:stream';
import { pqc } from '@pqc-sdk/core';
const pair = await pqc.keys.generate();
await Readable.toWeb(createReadStream('large-file.bin'))
.pipeThrough(pqc.encryptWebStream(pair.publicKey))
.pipeTo(Writable.toWeb(createWriteStream('large-file.bin.enc')));keys
readonlykeys:object
keys.deserialize
deserialize: {(
serialized):PqcKey; <A,U>(serialized,expected):PqcKey<A,U>; }
Call Signature
(
serialized):PqcKey
Rebuilds a key from the serialize format. Validates version, algorithm, use and length; on any problem it throws PqcError with code INVALID_SERIALIZED_KEY or INVALID_KEY.
Pass expected to assert the algorithm and use, getting back a narrow key type (e.g. PublicKey<'ml-kem-768'>) that drops straight into encrypt / sign without an as never cast. A mismatch throws WRONG_ALGORITHM or WRONG_KEY_USE.
Parameters
serialized
string
Returns
Example
ts
import { pqc } from '@pqc-sdk/core';
const token = pqc.keys.serialize((await pqc.keys.generate()).publicKey);
// Narrow to a typed key by asserting the expected algorithm and use:
const publicKey = pqc.keys.deserialize(token, { algorithm: 'ml-kem-768', use: 'public' });
const ciphertext = await pqc.encrypt('payload', publicKey);Call Signature
<
A,U>(serialized,expected):PqcKey<A,U>
Rebuilds a key from the serialize format. Validates version, algorithm, use and length; on any problem it throws PqcError with code INVALID_SERIALIZED_KEY or INVALID_KEY.
Pass expected to assert the algorithm and use, getting back a narrow key type (e.g. PublicKey<'ml-kem-768'>) that drops straight into encrypt / sign without an as never cast. A mismatch throws WRONG_ALGORITHM or WRONG_KEY_USE.
Type Parameters
A
A extends Algorithm
U
U extends KeyUse
Parameters
serialized
string
expected
ExpectedKey<A, U>
Returns
PqcKey<A, U>
Example
ts
import { pqc } from '@pqc-sdk/core';
const token = pqc.keys.serialize((await pqc.keys.generate()).publicKey);
// Narrow to a typed key by asserting the expected algorithm and use:
const publicKey = pqc.keys.deserialize(token, { algorithm: 'ml-kem-768', use: 'public' });
const ciphertext = await pqc.encrypt('payload', publicKey);keys.generate
generate: {():
Promise<KeyPair<"x-wing">>; <A>(options):Promise<KeyPair<A>>; (options?):Promise<KeyPair<Algorithm>>; }
Call Signature
():
Promise<KeyPair<"x-wing">>
Generates a post-quantum key pair. With no options it generates an X-Wing hybrid pair (X25519 + ML-KEM-768), ready for pqc.encrypt.
The no-argument default is hybrid because a break in either component still leaves the other standing — the same reasoning behind TLS 1.3's X25519MLKEM768, Signal's PQXDH, and the BSI and ANSSI recommendations. ML-KEM-768 is young, and a cryptanalytic result against it would leave a pure-PQ ciphertext with nothing to fall back on.
'ml-kem-768' remains fully supported and is the right choice in two cases: when FIPS certification scope matters (X-Wing is not covered by FIPS 203, so a compliance regime requiring a certified KEM needs the pure one), and when size or speed dominate (32 bytes less per envelope, and roughly 2–4× faster per operation — see docs/MIGRATION-0.8.md).
Returns
Promise<KeyPair<"x-wing">>
Example
ts
import { pqc } from '@pqc-sdk/core';
const encryption = await pqc.keys.generate(); // x-wing hybrid
const pureMlKem = await pqc.keys.generate({ algorithm: 'ml-kem-768' });
const signing = await pqc.keys.generate({ algorithm: 'ml-dsa-65' });Call Signature
<
A>(options):Promise<KeyPair<A>>
Generates a post-quantum key pair. With no options it generates an X-Wing hybrid pair (X25519 + ML-KEM-768), ready for pqc.encrypt.
The no-argument default is hybrid because a break in either component still leaves the other standing — the same reasoning behind TLS 1.3's X25519MLKEM768, Signal's PQXDH, and the BSI and ANSSI recommendations. ML-KEM-768 is young, and a cryptanalytic result against it would leave a pure-PQ ciphertext with nothing to fall back on.
'ml-kem-768' remains fully supported and is the right choice in two cases: when FIPS certification scope matters (X-Wing is not covered by FIPS 203, so a compliance regime requiring a certified KEM needs the pure one), and when size or speed dominate (32 bytes less per envelope, and roughly 2–4× faster per operation — see docs/MIGRATION-0.8.md).
Type Parameters
A
A extends Algorithm
Parameters
options
GenerateOptions<A> & object
Returns
Promise<KeyPair<A>>
Example
ts
import { pqc } from '@pqc-sdk/core';
const encryption = await pqc.keys.generate(); // x-wing hybrid
const pureMlKem = await pqc.keys.generate({ algorithm: 'ml-kem-768' });
const signing = await pqc.keys.generate({ algorithm: 'ml-dsa-65' });Call Signature
Generates a post-quantum key pair. With no options it generates an X-Wing hybrid pair (X25519 + ML-KEM-768), ready for pqc.encrypt.
The no-argument default is hybrid because a break in either component still leaves the other standing — the same reasoning behind TLS 1.3's X25519MLKEM768, Signal's PQXDH, and the BSI and ANSSI recommendations. ML-KEM-768 is young, and a cryptanalytic result against it would leave a pure-PQ ciphertext with nothing to fall back on.
'ml-kem-768' remains fully supported and is the right choice in two cases: when FIPS certification scope matters (X-Wing is not covered by FIPS 203, so a compliance regime requiring a certified KEM needs the pure one), and when size or speed dominate (32 bytes less per envelope, and roughly 2–4× faster per operation — see docs/MIGRATION-0.8.md).
Parameters
options?
Returns
Example
ts
import { pqc } from '@pqc-sdk/core';
const encryption = await pqc.keys.generate(); // x-wing hybrid
const pureMlKem = await pqc.keys.generate({ algorithm: 'ml-kem-768' });
const signing = await pqc.keys.generate({ algorithm: 'ml-dsa-65' });keys.serialize
serialize: (
key) =>string
Serializes a key to a portable string: pqcv1.<algorithm>.<use>.<base64url>.
Parameters
key
Returns
string
Example
ts
import { pqc } from '@pqc-sdk/core';
const pair = await pqc.keys.generate();
const token = pqc.keys.serialize(pair.publicKey);
// "pqcv1.ml-kem-768.public.h1q3…"sign
sign: (
data,secretKey,options?) =>Promise<Uint8Array<ArrayBufferLike>>
Signs data with ML-DSA-65 (FIPS 204) in hedged mode (randomized signing, the standard's default). Returns the 3309-byte signature.
Parameters
data
string | Uint8Array<ArrayBufferLike>
secretKey
SecretKey<"ml-dsa-65">
options?
Returns
Promise<Uint8Array<ArrayBufferLike>>
Example
ts
import { pqc } from '@pqc-sdk/core';
const pair = await pqc.keys.generate({ algorithm: 'ml-dsa-65' });
const signature = await pqc.sign(document, pair.secretKey);verify
verify: (
data,signature,publicKey,options?) =>Promise<boolean>
Verifies an ML-DSA-65 signature. Returns false for invalid or malformed signatures (it never throws because of a corrupted signature); it only throws if the key is not ML-DSA.
Parameters
data
string | Uint8Array<ArrayBufferLike>
signature
Uint8Array
publicKey
PublicKey<"ml-dsa-65">
options?
Returns
Promise<boolean>
Example
ts
import { pqc } from '@pqc-sdk/core';
const valid = await pqc.verify(document, signature, pair.publicKey);
if (!valid) throw new Error('invalid signature');Example
ts
import { pqc } from '@pqc-sdk/core';
// Encryption (ML-KEM-768 + AES-256-GCM)
const pair = await pqc.keys.generate();
const ciphertext = await pqc.encrypt('hello', pair.publicKey);
const plaintext = await pqc.decrypt(ciphertext, pair.secretKey);
// Hybrid encryption (X-Wing: X25519 + ML-KEM-768 + AES-256-GCM)
const hybrid = await pqc.keys.generate({ algorithm: 'x-wing' });
const hybridCiphertext = await pqc.encrypt('hello', hybrid.publicKey);
const hybridPlaintext = await pqc.decrypt(hybridCiphertext, hybrid.secretKey);
// Signatures (ML-DSA-65)
const signer = await pqc.keys.generate({ algorithm: 'ml-dsa-65' });
const signature = await pqc.sign('document', signer.secretKey);
await pqc.verify('document', signature, signer.publicKey); // trueFor payloads too large to hold in memory at once, see encryptStream/decryptStream (or the Web Streams adapters, encryptWebStream/decryptWebStream) — read decryptStream's documentation first, it has an incremental-release property one-shot decrypt does not.