Appearance
Encrypting files
The most common use case: encrypting a file for someone who has an ML-KEM-768 key pair, so that only that person can open it.
Setup: the recipient generates and shares their public key
bash
npx @pqc-sdk/cli keygen --out keys/
# keys/ml-kem-768.public.pqc → shared (mail, repo, wherever)
# keys/ml-kem-768.secret.pqc → NEVER leaves the recipient's machineEncrypt (sender side)
ts
import { readFile, writeFile } from 'node:fs/promises';
import { pqc } from '@pqc-sdk/core';
// The recipient's public key, received as serialized text. Asserting the
// expected algorithm and use narrows the type so it drops straight into encrypt.
const publicKey = pqc.keys.deserialize(
(await readFile('keys/ml-kem-768.public.pqc', 'utf8')).trim(),
{ algorithm: 'ml-kem-768', use: 'public' },
);
const contents = await readFile('confidential-report.pdf');
const ciphertext = await pqc.encrypt(contents, publicKey);
await writeFile('confidential-report.pdf.pqc', ciphertext);Asserting the algorithm and use
deserialize(token) returns the wide PqcKey type because the algorithm is only known at runtime. Pass { algorithm, use } to assert what you expect: you get back a narrow PublicKey<'ml-kem-768'> (no as never cast), and a mismatching token throws PqcError('WRONG_ALGORITHM') or 'WRONG_KEY_USE' before any crypto runs.
Decrypt (recipient side)
ts
import { readFile, writeFile } from 'node:fs/promises';
import { pqc } from '@pqc-sdk/core';
const secretKey = pqc.keys.deserialize(
(await readFile('keys/ml-kem-768.secret.pqc', 'utf8')).trim(),
{ algorithm: 'ml-kem-768', use: 'secret' },
);
const ciphertext = await readFile('confidential-report.pdf.pqc');
const contents = await pqc.decrypt(new Uint8Array(ciphertext), secretKey);
await writeFile('confidential-report.pdf', contents);If the .pqc file was altered in transit — even a single bit — decrypt throws DECRYPTION_FAILED instead of returning a corrupted PDF: AES-GCM authenticates the whole content.
Error handling
ts
try {
await pqc.decrypt(ciphertext, secretKey);
} catch (error) {
if (error instanceof PqcError) {
switch (error.code) {
case 'INVALID_CIPHERTEXT': // not a .pqc file, or truncated
case 'DECRYPTION_FAILED': // tampered with, or the key does not match
console.error(error.message);
}
}
}Large files
encrypt operates in memory: for files of hundreds of MB consider encrypting in chunks (each chunk is an independent encrypt with its own encapsulation) or wait for the SDK's streaming API. The per-message overhead is 1118 bytes.