lib/pdf/reader/encryption.ex

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