Skip to content

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 machine

Encrypt (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/decrypt operate fully in memory — fine for typical files, but not for anything too large to comfortably hold as one Uint8Array. For that, use encryptStream/decryptStream (or the Web Streams adapters) instead: see Streaming large files. The CLI already does this automatically — pqc encrypt/pqc decrypt switch to streaming above 8 MiB with no flag needed.