lib/pdf/reader/standard_security_handler_v5.rb



# coding: utf-8
# typed: true
# frozen_string_literal: true

require 'digest'
require 'openssl'

class PDF::Reader

  # class creates interface to encrypt dictionary for use in Decrypt
  class StandardSecurityHandlerV5

    attr_reader :key_length, :encrypt_key

    def initialize(opts = {})
      @key_length   = 256
      @O            = opts[:O]   # hash(32B) + validation salt(8B) + key salt(8B)
      @U            = opts[:U]   # hash(32B) + validation salt(8B) + key salt(8B)
      @OE           = opts[:OE]  # decryption key, encrypted w/ owner password
      @UE           = opts[:UE]  # decryption key, encrypted w/ user password
      @encrypt_key  = build_standard_key(opts[:password] || '')
    end

    # This handler supports AES-256 encryption defined in PDF 1.7 Extension Level 3
    def self.supports?(encrypt)
      return false if encrypt.nil?

      filter = encrypt.fetch(:Filter, :Standard)
      version = encrypt.fetch(:V, 0)
      revision = encrypt.fetch(:R, 0)
      algorithm = encrypt.fetch(:CF, {}).fetch(encrypt[:StmF], {}).fetch(:CFM, nil)
      (filter == :Standard) && (encrypt[:StmF] == encrypt[:StrF]) &&
          ((version == 5) && (revision == 5) && (algorithm == :AESV3))
    end

    ##7.6.2 General Encryption Algorithm
    #
    # Algorithm 1: Encryption of data using the RC4 or AES algorithms
    #
    # used to decrypt RC4/AES encrypted PDF streams (buf)
    #
    # buf - a string to decrypt
    # ref - a PDF::Reader::Reference for the object to decrypt
    #
    def decrypt( buf, ref )
      cipher = OpenSSL::Cipher.new("AES-#{@key_length}-CBC")
      cipher.decrypt
      cipher.key = @encrypt_key.dup
      cipher.iv = buf[0..15]
      cipher.update(buf[16..-1]) + cipher.final
    end

    private
    # Algorithm 3.2a - Computing an encryption key
    #
    # Defined in PDF 1.7 Extension Level 3
    #
    # if the string is a valid user/owner password, this will return the decryption key
    #
    def auth_owner_pass(password)
      if Digest::SHA256.digest(password + @O[32..39] + @U) == @O[0..31]
        cipher = OpenSSL::Cipher.new('AES-256-CBC')
        cipher.decrypt
        cipher.key = Digest::SHA256.digest(password + @O[40..-1] + @U)
        cipher.iv = "\x00" * 16
        cipher.padding = 0
        cipher.update(@OE) + cipher.final
      end
    end

    def auth_user_pass(password)
      if Digest::SHA256.digest(password + @U[32..39]) == @U[0..31]
        cipher = OpenSSL::Cipher.new('AES-256-CBC')
        cipher.decrypt
        cipher.key = Digest::SHA256.digest(password + @U[40..-1])
        cipher.iv = "\x00" * 16
        cipher.padding = 0
        cipher.update(@UE) + cipher.final
      end
    end

    def build_standard_key(pass)
      pass = pass.byteslice(0...127)   # UTF-8 encoded password. first 127 bytes

      encrypt_key   = auth_owner_pass(pass)
      encrypt_key ||= auth_user_pass(pass)

      raise PDF::Reader::EncryptedPDFError, "Invalid password (#{pass})" if encrypt_key.nil?
      encrypt_key
    end
  end
end