Tincture.PDF.Encrypt (Tincture v0.1.0)

Copy Markdown View Source

Standard security handler, revision 6 — AES-256.

Implements /V 5 /R 6, the encryption defined by PDF 2.0 (ISO 32000-2) and supported by Acrobat X and later, macOS Preview, pdf.js, PDFium, PDFBox, Ghostscript, qpdf and Poppler.

Earlier handlers are deliberately not implemented. Revisions 2 and 3 use RC4, which is broken. Revision 4 pairs AES-128 with an MD5-based key derivation. Revision 5 was an Adobe extension superseded by revision 6, which adds the hardening loop that makes password guessing expensive.

What this does and does not protect

A user password is real encryption. Without it the document cannot be read, because the file encryption key is recoverable only from the password.

An owner password on its own is not. The document is encrypted under the empty user password, so any reader can open it; the permission flags are advisory and enforced only by a viewer that chooses to. qpdf --decrypt removes them without knowing the password. That is a property of the PDF format, not of this implementation, and callers should not be led to believe otherwise.

Structure

A random 32-byte file encryption key is generated per document and never derived from a password. Each password instead protects a wrapped copy of that key: /U and /UE for the user password, /O and /OE for the owner password. Opening the document means deriving a key from the supplied password, using it to unwrap the file key, and decrypting with that.

Under revision 6 every string and stream is encrypted with the file key directly — unlike revision 4, which derived a distinct key per object.

Summary

Types

A permission a viewer is asked to allow. Everything not listed is denied.

t()

Everything needed to encrypt a document's strings and streams.

Functions

Encrypt a string or stream body.

The /Encrypt dictionary body, to be written as its own object.

Encrypt the strings and stream body inside one serialised object.

The document identifier, written into the trailer's /ID.

Build an encryption context.

The /P value for a permission list — exposed for testing and inspection.

Types

permission()

@type permission() ::
  :print
  | :modify
  | :copy
  | :annotate
  | :fill_forms
  | :extract_for_accessibility
  | :assemble
  | :print_high_quality

A permission a viewer is asked to allow. Everything not listed is denied.

These are advisory. See the note on owner passwords in the module docs.

t()

@type t() :: %{key: binary(), encrypt_dictionary: String.t(), id: binary()}

Everything needed to encrypt a document's strings and streams.

Functions

encrypt(data, map)

@spec encrypt(binary(), t()) :: binary()

Encrypt a string or stream body.

A fresh initialisation vector is generated per call and prepended to the ciphertext, which is what the specification requires and what makes two identical strings in one document encrypt differently.

encrypt_dictionary(map)

@spec encrypt_dictionary(t()) :: String.t()

The /Encrypt dictionary body, to be written as its own object.

encrypt_object(body, context)

@spec encrypt_object(iodata(), t()) :: iodata()

Encrypt the strings and stream body inside one serialised object.

Applied to a finished object body rather than at each point a string is written, because every string in the document has to be encrypted and threading a context through every emitter would touch far more code than it would protect.

Literal (...) and hex <...> strings are encrypted in place; << and >> are dictionary delimiters and are left alone. A stream body is encrypted whole and its /Length corrected, since the ciphertext is longer than the plaintext by the initialisation vector plus padding.

id(map)

@spec id(t()) :: binary()

The document identifier, written into the trailer's /ID.

new(opts \\ [])

@spec new(keyword()) :: t()

Build an encryption context.

Options

  • :user_password — required to open the document. Defaults to "", which means anyone can open it and only the permissions apply.
  • :owner_password — grants full rights. Defaults to the user password.
  • :permissions — the list of permission/0 values a viewer is asked to allow. Defaults to all of them.
  • :encrypt_metadata — whether document metadata is encrypted along with the content. Defaults to true.

permission_flags(permissions)

@spec permission_flags([permission()]) :: integer()

The /P value for a permission list — exposed for testing and inspection.