defmodule Pdf.Reader.Encryption.ObjectKey do @moduledoc """ Derives the per-object encryption key used for V1, V2, and V4 Standard Security Handler streams and strings. Implements Algorithm 1 from PDF 1.7 § 7.6.2, which produces an object- specific key from the file encryption key, the object number, the generation number, and the cipher family. ## Algorithm 1 (PDF 1.7 § 7.6.2) 1. Start with the file encryption key bytes. 2. Append `obj_num` as 3 little-endian bytes (low-order first). 3. Append `gen_num` as 2 little-endian bytes (low-order first). 4. If the cipher is AES, append the 4-byte literal `"sAlT"` (0x73 0x41 0x6c 0x54). 5. Compute MD5 over the resulting byte sequence. 6. Truncate to `min(byte_size(file_key) + 5, 16)` bytes. This function is NOT used for V5 (AES-256); V5 uses the file key directly without per-object derivation. ## Spec references - PDF 1.7 (ISO 32000-1) § 7.6.2 — General Encryption Algorithm: https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf - RFC 1321 (MD5): https://www.rfc-editor.org/rfc/rfc1321.html - Mozilla pdf.js src/core/crypto.js #buildObjectKey (Apache-2.0 cross-check): https://github.com/mozilla/pdf.js/blob/master/src/core/crypto.js """ # "sAlT" — appended before MD5 when the effective cipher is AES (V4 AES-128) @salt <<0x73, 0x41, 0x6C, 0x54>> # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- @doc """ Derives a per-object encryption key. ## Parameters - `file_key` — the file encryption key derived by Algorithm 2 (V1/V2/V4). Typically 5 bytes (V1 RC4-40) or up to 16 bytes (V2/V4 RC4-128 / AES-128). - `obj_num` — the PDF object number (non-negative integer). - `gen_num` — the PDF generation number (non-negative integer; usually 0 for most in-use objects). - `cipher` — the effective cipher for this object. Pass `:aes_128` for AES; any other value (including `:rc4`) is treated as RC4 (no "sAlT" suffix). ## Returns A binary of `min(byte_size(file_key) + 5, 16)` bytes. """ @spec derive(binary(), non_neg_integer(), non_neg_integer(), :aes_128 | :rc4) :: binary() def derive(file_key, obj_num, gen_num, cipher) when is_binary(file_key) do input = file_key <> <> <> <> <> salt_suffix(cipher) hash = :crypto.hash(:md5, input) truncate_to = min(byte_size(file_key) + 5, 16) binary_part(hash, 0, truncate_to) end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp salt_suffix(:aes_128), do: @salt defp salt_suffix(_), do: <<>> end