defmodule Pdf.Reader.Encryption do @moduledoc """ Facade module for PDF Standard Security Handler authentication and decryption. Dispatches to the appropriate version-specific module (`V1V2`, `V4`, `V5`) based on the `/V` field of the parsed `%StandardHandler{}` struct. This module does NOT parse the Encrypt dict — that is handled by `Pdf.Reader.Encryption.StandardHandler.parse/2`. This module only: 1. Dispatches `unlock/3` to the correct version module. 2. Returns the handler with `:file_key` populated on success. ## Authentication flow 1. Try user password via `authenticate_user/2` for the detected version. 2. If that fails (`:error`), try owner password via `authenticate_owner/2`. 3. Return `{:ok, %StandardHandler{file_key: key}}` or `:error`. ## Supported versions | `/V` | Module | Revision | |------|-------------|----------| | 1 | `V1V2` | R=2 | | 2 | `V1V2` | R=3/4 | | 4 | `V4` | R=4 | | 5 | `V5` | R=6 only | V5/R5 (deprecated Acrobat X beta) is rejected by `V5.authenticate_user/2` with `{:error, :encrypted_unsupported_handler}` per R-ENC25. ## Spec references - PDF 1.7 (ISO 32000-1) § 7.6 — Standard Security Handler (V1/V2/V4): https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf - PDF 2.0 (ISO 32000-2) § 7.6 — Standard Security Handler (V5/R6): https://www.pdfa.org/wp-content/uploads/2023/04/ISO_32000_2_2020_PDF_2.0_FDIS.pdf - Mozilla pdf.js src/core/crypto.js (Apache-2.0 reference implementation): https://github.com/mozilla/pdf.js/blob/master/src/core/crypto.js """ alias Pdf.Reader.Encryption.{StandardHandler, V1V2, V4, V5} # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- @doc """ Attempts to unlock an encrypted PDF handler with the given password. Tries the password as user, then as owner. Returns the handler with `:file_key` populated on success. ## Parameters - `password` — the plaintext password string. - `handler` — a `%StandardHandler{}` parsed from the Encrypt dict (`:file_key` is nil). - `_doc` — reserved for future use (e.g., doc reference); ignored for now. ## Returns - `{:ok, %StandardHandler{file_key: key}}` — authenticated; key is populated. - `:error` — wrong password (tried as both user and owner). - `{:error, :encrypted_unsupported_handler}` — version unsupported or RC4 unavailable. """ @spec unlock(binary(), StandardHandler.t(), map()) :: {:ok, StandardHandler.t()} | :error | {:error, :encrypted_unsupported_handler} def unlock(password, %StandardHandler{} = handler, _doc) when is_binary(password) do case dispatch_user(password, handler) do {:ok, file_key} -> {:ok, %{handler | file_key: file_key}} :error -> # Try as owner password case dispatch_owner(password, handler) do {:ok, file_key} -> {:ok, %{handler | file_key: file_key}} other -> other end {:error, _} = err -> err end end # --------------------------------------------------------------------------- # Private — version dispatch # --------------------------------------------------------------------------- defp dispatch_user(password, %StandardHandler{version: v} = handler) when v in [1, 2] do V1V2.authenticate_user(password, handler) end defp dispatch_user(password, %StandardHandler{version: 4} = handler) do V4.authenticate_user(password, handler) end defp dispatch_user(password, %StandardHandler{version: 5} = handler) do V5.authenticate_user(password, handler) end defp dispatch_user(_password, _handler) do {:error, :encrypted_unsupported_handler} end defp dispatch_owner(password, %StandardHandler{version: v} = handler) when v in [1, 2] do V1V2.authenticate_owner(password, handler) end defp dispatch_owner(password, %StandardHandler{version: 4} = handler) do V4.authenticate_owner(password, handler) end defp dispatch_owner(password, %StandardHandler{version: 5} = handler) do V5.authenticate_owner(password, handler) end defp dispatch_owner(_password, _handler) do {:error, :encrypted_unsupported_handler} end end