# -*- encoding: utf-8 -*-

require 'test_helper'
require 'hexapdf/encryption/security_handler'
require 'hexapdf/document'
require 'hexapdf/stream'

describe HexaPDF::Encryption::EncryptionDictionary do
  before do
    @document = HexaPDF::Document.new
    @dict = HexaPDF::Encryption::EncryptionDictionary.new({}, document: @document)
    @dict[:Filter] = :Standard
    @dict[:V] = 1
  end

  it "must be an indirect object" do
    assert(@dict.must_be_indirect?)
  end

  it "validates the /Length field when /V=2" do
    @dict[:V] = 2
    refute(@dict.validate)

    @dict[:Length] = 32
    refute(@dict.validate)
    @dict[:Length] = 136
    refute(@dict.validate)
    @dict[:Length] = 55
    refute(@dict.validate)

    @dict[:Length] = 120
    assert(@dict.validate)
  end
end

describe HexaPDF::Encryption::SecurityHandler do
  class TestHandler < HexaPDF::Encryption::SecurityHandler

    attr_accessor :strf, :myopt
    public :dict

    def prepare_encryption(**_options)
      dict[:Filter] = :Test
      @key = "a" * key_length
      @strf ||= :aes
      @stmf ||= :arc4
      @eff ||= :identity
      [@key, @strf, @stmf, @eff]
    end

    def prepare_decryption(myopt: nil)
      @myopt = myopt
      @key = "a" * key_length
    end

  end

  before do
    @document = HexaPDF::Document.new
    @obj = @document.add({})
    @handler = TestHandler.new(@document)
  end

  describe "class methods" do
    before do
      @document.config['encryption.filter_map'][:Test] = TestHandler
    end

    describe "set_up_encryption" do
      it "fails if the requested security handler cannot be found" do
        assert_raises(HexaPDF::EncryptionError) do
          HexaPDF::Encryption::SecurityHandler.set_up_encryption(@document, :non_standard)
        end
      end

      it "updates the trailer's /Encrypt entry to be wrapped by an encryption dictionary" do
        HexaPDF::Encryption::SecurityHandler.set_up_encryption(@document, :Test)
        assert_kind_of(HexaPDF::Encryption::EncryptionDictionary, @document.trailer[:Encrypt])
      end

      it "returns the frozen security handler" do
        handler = HexaPDF::Encryption::SecurityHandler.set_up_encryption(@document, :Test)
        assert(handler.frozen?)
      end
    end

    describe "set_up_decryption" do
      it "fails if the document has no /Encrypt dictionary" do
        exp = assert_raises(HexaPDF::EncryptionError) do
          HexaPDF::Encryption::SecurityHandler.set_up_decryption(@document)
        end
        assert_match(/No \/Encrypt/i, exp.message)
      end

      it "fails if the requested security handler cannot be found" do
        @document.trailer[:Encrypt] = {Filter: :NonStandard}
        assert_raises(HexaPDF::EncryptionError) do
          HexaPDF::Encryption::SecurityHandler.set_up_decryption(@document)
        end
      end

      it "updates the trailer's /Encrypt entry to be wrapped by an encryption dictionary" do
        @document.trailer[:Encrypt] = {Filter: :Test,
                                       V: HexaPDF::Object.new(1, oid: 1, document: @document)}
        HexaPDF::Encryption::SecurityHandler.set_up_decryption(@document)
        assert_kind_of(HexaPDF::Encryption::EncryptionDictionary, @document.trailer[:Encrypt])
        assert_equal({Filter: :Test, V: 1}, @document.trailer[:Encrypt].value)
      end

      it "returns the frozen security handler" do
        @document.trailer[:Encrypt] = {Filter: :Test, V: 1}
        handler = HexaPDF::Encryption::SecurityHandler.set_up_decryption(@document)
        assert(handler.frozen?)
      end
    end
  end

  it "doesn't have a valid encryption key directly after creation" do
    refute(@handler.encryption_key_valid?)
  end

  describe "set_up_encryption" do
    it "sets the correct /V value for the given key length and algorithm" do
      [[40, :arc4, 1], [128, :arc4, 2], [128, :arc4, 4],
       [128, :aes, 4], [256, :aes, 5]].each do |length, algorithm, version|
        @handler.set_up_encryption(key_length: length, algorithm: algorithm, force_v4: version == 4)
        assert_equal(version, @handler.dict[:V])
      end
    end

    it "sets the correct /Length value for the given key length" do
      [[40, nil], [48, 48], [128, 128], [256, nil]].each do |key_length, result|
        algorithm = (key_length == 256 ? :aes : :arc4)
        @handler.set_up_encryption(key_length: key_length, algorithm: algorithm)
        assert(result == @handler.dict[:Length])
      end

      # Work-around buggy software
      @handler.set_up_encryption(key_length: 128, algorithm: :aes)
      assert_equal(4, @handler.dict[:V])
      assert_equal(128, @handler.dict[:Length])
    end

    it "calls the prepare_encryption method" do
      @handler.set_up_encryption
      assert_equal(:Test, @handler.dict[:Filter])
    end

    it "returns the generated encryption dictionary wrapped in an encryption class" do
      dict = @handler.set_up_encryption
      assert_kind_of(HexaPDF::Encryption::EncryptionDictionary, dict)
    end

    it "set's up the handler for encryption" do
      [:arc4, :aes].each do |algorithm|
        @handler.set_up_encryption(key_length: 128, algorithm: algorithm)
        @obj[:X] = @handler.encrypt_string('data', @obj)
        assert_equal('data', @handler.decrypt(@obj)[:X])
      end
    end

    it "generates a valid encryption key" do
      @document.trailer[:Encrypt] = @handler.set_up_encryption
      assert(@handler.encryption_key_valid?)
    end

    it "provides correct encryption details" do
      @handler.set_up_encryption
      assert_equal({version: 4, string_algorithm: :aes, stream_algorithm: :arc4,
                    embedded_file_algorithm: :identity, key_length: 128},
                   @handler.encryption_details)
      assert_equal(HexaPDF::Encryption::Identity, @handler.send(:embedded_file_algorithm))
      assert_equal(HexaPDF::Encryption::FastAES, @handler.send(:string_algorithm))
      assert_equal(HexaPDF::Encryption::FastARC4, @handler.send(:stream_algorithm))
    end

    it "fails for unsupported encryption key lengths" do
      exp = assert_raises(HexaPDF::UnsupportedEncryptionError) do
        @handler.set_up_encryption(key_length: 43)
      end
      assert_match(/Invalid key length/i, exp.message)
    end

    it "fails for unsupported encryption algorithms" do
      exp = assert_raises(HexaPDF::UnsupportedEncryptionError) do
        @handler.set_up_encryption(algorithm: :test)
      end
      assert_match(/Unsupported encryption algorithm/i, exp.message)
    end

    it "fails for the aes algorithm with key lengths != 128 or 256" do
      exp = assert_raises(HexaPDF::UnsupportedEncryptionError) do
        @handler.set_up_encryption(algorithm: :aes, key_length: 40)
      end
      assert_match(/AES algorithm.*key length/i, exp.message)
    end

    it "fails for the arc4 algorithm with a key length of 256" do
      exp = assert_raises(HexaPDF::UnsupportedEncryptionError) do
        @handler.set_up_encryption(algorithm: :arc4, key_length: 256)
      end
      assert_match(/ARC4 algorithm.*key length/i, exp.message)
    end
  end

  describe "set_up_decryption" do
    it "wraps the given hash in an encryption dictionary class, uses it for its dict, returns it" do
      dict = @handler.set_up_decryption({Filter: :test, V: 1})
      assert_equal(dict, @handler.dict)
      assert_kind_of(HexaPDF::Encryption::EncryptionDictionary, @handler.dict)
      assert_equal({Filter: :test, V: 1}, @handler.dict.value)
    end

    it "doesn't modify the trailer's /Encrypt dictionary" do
      @handler.set_up_decryption({Filter: :test, V: 4, Length: 128})
      assert_nil(@document.trailer[:Encrypt])
    end

    it "calls prepare_decryption" do
      @handler.set_up_decryption({Filter: :test, V: 4, Length: 128}, myopt: 5)
      assert_equal(5, @handler.myopt)
    end

    it "selects the correct algorithm based on the /V and /CF values" do
      @enc = @handler.dup

      [
        [:arc4, 40, {V: 1}],
        [:arc4, 80, {V: 2, Length: 80}],
        [:arc4, 128, {V: 4, StrF: :Mine, CF: {Mine: {CFM: :V2}}}],
        [:aes, 128, {V: 4, StrF: :Mine, CF: {Mine: {CFM: :AESV2}}}],
        [:aes, 256, {V: 5, StrF: :Mine, CF: {Mine: {CFM: :AESV3}}}],
        [:identity, 128, {V: 4, StrF: :Mine, CF: {Mine: {CFM: :None}}}],
        [:identity, 128, {V: 4, CF: {Mine: {CFM: :AESV2}}}],
      ].each do |alg, length, dict|
        dict[:Filter] = :Test
        @enc.strf = alg
        @enc.set_up_encryption(key_length: length, algorithm: (alg == :identity ? :aes : alg))
        @obj[:X] = @enc.encrypt_string(+'data', @obj)
        @handler.set_up_decryption(dict)
        assert_equal('data', @handler.decrypt(@obj)[:X])
      end
    end

    it "selects the correct algorithm for string, stream and embedded file decryption" do
      @handler.set_up_decryption({Filter: :Test, V: 4, StrF: :Mine, StmF: :Mine, EFF: :Mine,
                                  CF: {Mine: {CFM: :V2}}})
      assert_equal(HexaPDF::Encryption::FastARC4, @handler.send(:embedded_file_algorithm))
      assert_equal(HexaPDF::Encryption::FastARC4, @handler.send(:string_algorithm))
      assert_equal(HexaPDF::Encryption::FastARC4, @handler.send(:stream_algorithm))
    end

    it "provides correct encryption details" do
      @handler.set_up_decryption({Filter: :test, V: 2, Length: 128}, myopt: 5)
      assert_equal({version: 2, string_algorithm: :arc4, stream_algorithm: :arc4,
                    embedded_file_algorithm: :arc4, key_length: 128},
                   @handler.encryption_details)
    end

    it "fails if the encryption dictionary is not valid" do
      exp = assert_raises(HexaPDF::Error) do
        @handler.set_up_decryption({V: 5})
      end
      assert_match(/Validation error for encryption dictionary.*Required field Filter/i, exp.message)
    end

    it "fails for unsupported /V values in the dict" do
      exp = assert_raises(HexaPDF::UnsupportedEncryptionError) do
        @handler.set_up_decryption({Filter: :Text, V: 3})
      end
      assert_match(/Unsupported encryption version/i, exp.message)
    end

    it "fails for unsupported crypt filter encryption methods" do
      exp = assert_raises(HexaPDF::UnsupportedEncryptionError) do
        @handler.set_up_decryption({Filter: :Test, V: 4, StrF: :Mine, CF: {Mine: {CFM: :Unknown}}})
      end
      assert_match(/Unsupported encryption method/i, exp.message)
    end
  end

  describe "decrypt" do
    before do
      @handler.set_up_decryption({Filter: :Test, V: 1})
      @encrypted = @handler.encrypt_string('string', @obj)
      @obj.value = {Key: @encrypted.dup, Array: [@encrypted.dup], Hash: {Another: @encrypted.dup}}
    end

    it "decrypts all strings in an object" do
      @handler.decrypt(@obj)
      assert_equal('string', @obj[:Key])
      assert_equal('string', @obj[:Array][0])
      assert_equal('string', @obj[:Hash][:Another])
    end

    it "decrypts the content of a stream object" do
      data = HexaPDF::StreamData.new(proc { @encrypted })
      obj = @document.wrap({}, oid: @obj.oid, stream: data)
      @handler.decrypt(obj)
      assert_equal('string', obj.stream)
    end

    it "doesn't decrypt a document's Encrypt dictionaries" do
      @document = HexaPDF::Document.new
      @document.trailer[:Encrypt] = @document.add({Key: "Something"})
      @document.revisions.add
      @document.trailer[:Encrypt] = @document.add({Key: "Otherthing"})
      @handler = TestHandler.new(@document)

      assert_equal("Something",
                   @handler.decrypt(@document.revisions.all[0].trailer[:Encrypt])[:Key])
      assert_equal("Otherthing",
                   @handler.decrypt(@document.revisions.all[1].trailer[:Encrypt])[:Key])
    end

    it "defers handling encryption to a Crypt filter is specified" do
      data = HexaPDF::StreamData.new(proc { 'mydata' }, filter: :Crypt)
      obj = @document.wrap({}, oid: 1, stream: data)
      @handler.decrypt(obj)
      assert_equal('mydata', obj.stream)
    end

    it "doesn't decrypt XRef streams" do
      @obj[:Type] = :XRef
      assert_equal(@encrypted, @handler.decrypt(@obj)[:Key])
    end

    it "doesn't decrypt the /Contents of a signature dictionary" do
      @obj[:Type] = :Sig
      @obj[:Contents] = "test"
      assert_equal("test", @handler.decrypt(@obj)[:Contents])
    end

    it "enhances a thrown EncryptionError by setting the PDF object" do
      @document.config['encryption.on_decryption_error'] = proc { true }
      @handler.set_up_encryption(key_length: 256)
      error = assert_raises(HexaPDF::EncryptionError) { @handler.decrypt(@obj) }
      assert_match(/Object \(1,0\):/, error.message)
    end

    it "uses the encryption.on_decryption_error configuration option" do
      @handler.set_up_encryption(key_length: 256)
      @handler.decrypt(@obj)
      assert_equal('', @obj[:Key])

      @obj[:Key] = @encrypted
      called = false
      @document.config['encryption.on_decryption_error'] = proc do |obj, msg|
        assert_same(@obj, obj)
        assert_match(/32 \+ 16/, msg)
        called = true
      end
      assert_raises(HexaPDF::EncryptionError) { @handler.decrypt(@obj) }
      assert(called)
    end

    it "fails if V < 5 and the object number changes" do
      @obj.oid = 55
      @handler.decrypt(@obj)
      refute_equal('string', @obj[:Key])
    end
  end

  describe "encryption" do
    before do
      @handler.set_up_encryption(key_length: 128, algorithm: :arc4)
      @stream = @document.wrap({}, oid: 1, stream: HexaPDF::StreamData.new(proc { "string" }))
    end

    it "encrypts strings of indirect objects" do
      @obj[:Key] = @handler.encrypt_string('string', @obj)
      assert_equal('string', @handler.decrypt(@obj)[:Key])
    end

    it "encrypts streams" do
      result = collector(@handler.encrypt_stream(@stream))
      @stream.stream = HexaPDF::StreamData.new(proc { result })
      assert_equal('string', @handler.decrypt(@stream).stream)
    end

    it "doesn't encrypt strings in a document's Encrypt dictionary" do
      @document.trailer[:Encrypt] = @handler.dict
      str = 'string'
      result = @handler.encrypt_string(str, @document.trailer[:Encrypt])
      assert_equal('string', result)
      refute_same(str, result)
    end

    it "doesn't encrypt XRef streams" do
      @stream[:Type] = :XRef
      assert_equal('string', @handler.encrypt_stream(@stream).resume)
    end

    it "defers encrypting to a Crypt filter if specified" do
      @stream.set_filter(:Crypt)
      assert_equal('string', @handler.encrypt_stream(@stream).resume)

      @stream.set_filter([:Crypt])
      assert_equal('string', @handler.encrypt_stream(@stream).resume)
    end

    it "doesn't encrypt the /Contents key of signature dictionaries" do
      @obj[:Type] = :Sig
      @obj[:Contents] = "test"
      refute_equal('test', @handler.encrypt_string("test", @obj))
      assert_equal('test', @handler.encrypt_string(@obj[:Contents], @obj))
    end
  end

  it "works correctly with different decryption and encryption handlers" do
    test_file = File.join(TEST_DATA_DIR, 'standard-security-handler', 'nopwd-arc4-40bit-V1.pdf')
    doc = HexaPDF::Document.new(io: StringIO.new(File.binread(test_file)))
    doc.encrypt(algorithm: :aes, password: 'test')
    out = StringIO.new(''.b)
    doc.write(out, update_fields: false)

    assert_raises(HexaPDF::EncryptionError) { HexaPDF::Document.new(io: out) }
    doc = HexaPDF::Document.new(io: out, decryption_opts: {password: 'test'})
    assert_equal('D:20150409164600', doc.trailer[:Info].value[:ModDate])
  end
end
