Skip to content

Encrypted files

lamina reads files encrypted with the standard security handler: RC4 (R2–R4), AES-128 (R4) and AES-256 (R5, R6), including crypt filters and cleartext metadata. Strings and streams are decrypted as the file loads.

  • Pass the password with Document.open(bytes, { password }). It’s tried as the user password, then as the owner password.
  • Unicode passwords: the password is tried as you give it, then in NFKC form after wrong-password. So R2–R4 passwords reach the core unchanged, and R5/R6 passwords match SASLprep’s normalisation.
  • No password, or the wrong one, fails with password-required or wrong-password. Ask the user and try again: nothing else has changed.
  • Saving: an encrypted file saves incrementally, or as a plaintext full rewrite with removeEncryption: true (see saving). Writing encrypted files is planned.

This example opens an upload with a password and returns the outcome as a value. Its test opens an AES-256 (R6) fixture with the right password, then with none and a wrong one:

packages/pdf/examples/open-untrusted.ts
// Open bytes from someone else: a password, tighter limits than the defaults, and the outcome as
// a value rather than an exception (the docs site's encrypted files, limits and errors guides).
// It has no Rust golden: it writes nothing; test/examples.test.ts checks what it reports.
import { Document, isLaminaError } from "@lamina/pdf";
/** What opening a file gave: its page count and warning codes, or why it didn't open. */
export type Opened =
| { readonly ok: true; readonly pages: number; readonly warnings: readonly string[] }
| { readonly ok: false; readonly code: string; readonly hint: string | undefined };
/**
* Opens `bytes` with `password` (empty: none) and limits for an upload endpoint: decoded streams
* of at most 16 MiB each and 64 MiB in all. Needs `init()`.
*/
export function openUntrusted(bytes: Uint8Array, password = ""): Opened {
try {
const doc = Document.open(bytes, {
...(password === "" ? {} : { password }),
limits: { maxStreamDecoded: 16 << 20, maxDecodedTotal: 64 << 20 },
});
try {
// Repairs and dropped values are warnings: the file opened, but not exactly as written.
return { ok: true, pages: doc.pageCount, warnings: doc.warnings.map((w) => w.code) };
} finally {
doc.free();
}
} catch (e) {
// `password-required`, `wrong-password`, `not-pdf`, `limit-exceeded`, …: a coded LaminaError
// whose message and hint never contain text from the file (ADR 0026 §4).
if (isLaminaError(e)) return { ok: false, code: e.code, hint: e.hint };
throw e;
}
}

The password goes only to the WebAssembly module. lamina never stores or logs it, and never puts it in an error.