File: aes_v2_security_handler_spec.rb

package info (click to toggle)
ruby-pdf-reader 2.15.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 33,512 kB
  • sloc: ruby: 11,959; sh: 46; makefile: 11
file content (120 lines) | stat: -rw-r--r-- 4,205 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# typed: false
# coding: utf-8

require 'openssl'

describe PDF::Reader::AesV2SecurityHandler do
  let(:key) { "test_encryption_key" }
  let(:handler) { PDF::Reader::AesV2SecurityHandler.new(key) }
  let(:reference) { PDF::Reader::Reference.new(1, 0) }

  describe "#decrypt" do
    context "with ciphertext that is not a multiple of 16 bytes" do
      it "raises MalformedPDFError" do
        invalid_buf = "short"
        expect {
          handler.decrypt(invalid_buf, reference)
        }.to raise_error(PDF::Reader::MalformedPDFError, "Ciphertext not a multiple of 16")
      end
    end

    context "with exactly 16 bytes (IV only)" do
      it "returns empty string" do
        iv_only = "0123456789abcdef"
        result = handler.decrypt(iv_only, reference)
        expect(result).to eq("")
      end
    end

    context "with valid padded ciphertext" do
      it "decrypts successfully with PKCS#7 padding" do
        plaintext = "Hello, World!"
        buf = encrypt_test_data(plaintext, key, reference, padding: true)

        result = handler.decrypt(buf, reference)
        expect(result).to eq(plaintext)
      end
    end

    context "with ciphertext without padding and valid key" do
      it "should decrypt successfully with no padding" do
        plaintext = "1234567890123456" # exactly 16 bytes
        buf = encrypt_test_data(plaintext, key, reference, padding: false)

        result = handler.decrypt(buf, reference)
        expect(result).to eq(plaintext)
      end
    end

    context "with ciphertext without padding and invalid key" do
      it "returns incorrect data when key is wrong" do
        wrong_key = "wrong_key_here!"
        wrong_handler = PDF::Reader::AesV2SecurityHandler.new(wrong_key)

        plaintext = "1234567890123456"
        buf = encrypt_test_data(plaintext, key, reference, padding: false)

        # Try to decrypt with wrong key - should return garbage, not raise error
        result = wrong_handler.decrypt(buf, reference)
        expect(result).not_to eq(plaintext)
        expect(result).to be_a(String)
      end
    end

    context "with padded ciphertext and invalid key" do
      it "returns incorrect data when key is wrong" do
        wrong_key = "wrong_key_here!"
        wrong_handler = PDF::Reader::AesV2SecurityHandler.new(wrong_key)

        plaintext = "Hello, World!"
        buf = encrypt_test_data(plaintext, key, reference, padding: true)

        # Try to decrypt with wrong key - should return garbage, not raise error
        result = wrong_handler.decrypt(buf, reference)
        expect(result).not_to eq(plaintext)
        expect(result).to be_a(String)
      end
    end

    context "with malformed ciphertext" do
      it "returns a string with garbage content" do
        # Create invalid ciphertext (random bytes, must be multiple of 16)
        iv = "0123456789abcdef"
        corrupted_data = "corrupted_data16" # exactly 16 bytes
        buf = iv + corrupted_data

        result = handler.decrypt(buf, reference)
        expect(result).to be_a(String)
      end
    end
  end

  private

  # Helper method to create encrypted test data
  # @param plaintext [String] the text to encrypt
  # @param encryption_key [String] the encryption key to use
  # @param ref [PDF::Reader::Reference] the PDF reference for key generation
  # @param padding [Boolean] whether to use PKCS#7 padding
  # @return [String] IV + ciphertext ready for decrypt method
  def encrypt_test_data(plaintext, encryption_key, ref, padding: true)
    # Generate the same object key that the handler would generate
    obj_key = encryption_key.dup
    (0..2).each { |e| obj_key << (ref.id >> e*8 & 0xFF) }
    (0..1).each { |e| obj_key << (ref.gen >> e*8 & 0xFF) }
    obj_key << 'sAlT'
    length = obj_key.length < 16 ? obj_key.length : 16
    digest_key = Digest::MD5.digest(obj_key)[0, length]

    # Encrypt with or without padding
    cipher = OpenSSL::Cipher.new("AES-#{length << 3}-CBC")
    cipher.encrypt
    cipher.padding = 0 unless padding
    cipher.key = digest_key
    iv = cipher.random_iv
    ciphertext = cipher.update(plaintext) + cipher.final

    # Return IV + ciphertext for the handler
    iv + ciphertext
  end
end