Class: Xlsxrb::Ooxml::Crypto::Standard

Inherits:
Object
  • Object
show all
Defined in:
lib/xlsxrb/ooxml/crypto/standard.rb,
sig/generated/xlsxrb/ooxml/crypto/standard.rbs

Overview

Implements Microsoft Office Standard Encryption specified in [MS-OFFCRYPTO] Section 2.3.6.

Constant Summary collapse

CSP_NAME =

Returns:

  • (Object)
"Microsoft Enhanced RSA and AES Cryptographic Provider\x00".encode("UTF-16LE").b.freeze
ITERATION_COUNT =

Returns:

  • (::Integer)
50_000

Class Method Summary collapse

Class Method Details

.decrypt(encryption_info_bytes, encrypted_package_bytes, password) ⇒ Object

Decrypts a Standard-encrypted package.

Parameters:

  • encryption_info_bytes (Object)
  • encrypted_package_bytes (Object)
  • password (Object)

Returns:

  • (Object)


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
# File 'lib/xlsxrb/ooxml/crypto/standard.rb', line 18

def decrypt(encryption_info_bytes, encrypted_package_bytes, password)
  salt, encrypted_verifier, verifier_hash_size, encrypted_verifier_hash = parse_encryption_info(encryption_info_bytes)
  password_str = password.to_s

  # Derive key K
  k = derive_key(password_str, salt)

  # Verify password
  verifier = aes_ecb_decrypt(k, encrypted_verifier)
  expected_hash = OpenSSL::Digest::SHA1.digest(verifier)

  decrypted_hash = aes_ecb_decrypt(k, encrypted_verifier_hash)[0, verifier_hash_size]
  raise Xlsxrb::InvalidPasswordError, "Standard encryption password verification failed" unless OpenSSL.secure_compare(decrypted_hash, expected_hash)

  # Decrypt package stream
  raise Xlsxrb::DecryptionError, "Encrypted package stream is too short" if encrypted_package_bytes.bytesize < 8

  total_size = encrypted_package_bytes[0, 8].unpack1("Q<")
  raise Xlsxrb::DecryptionError, "Encrypted package size is invalid or exceeds limits" if total_size.negative? || total_size > 0x400_000_000

  encrypted_data = encrypted_package_bytes[8..] || "".b

  decrypted = aes_ecb_decrypt(k, encrypted_data)
  decrypted[0, total_size] || "".b
end

.encrypt(plain_bytes, password) ⇒ Object

Encrypts plain zip data into Standard Encryption streams.

Parameters:

  • plain_bytes (Object)
  • password (Object)

Returns:

  • (Object)


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
# File 'lib/xlsxrb/ooxml/crypto/standard.rb', line 45

def encrypt(plain_bytes, password)
  password_str = password.to_s
  salt = SecureRandom.random_bytes(16)
  verifier = SecureRandom.random_bytes(16)

  # Derive key K
  k = derive_key(password_str, salt)

  # Encrypt verifier
  encrypted_verifier = aes_ecb_encrypt(k, verifier)

  # Encrypt verifier hash
  verifier_hash = OpenSSL::Digest::SHA1.digest(verifier)
  padded_hash = verifier_hash.ljust(32, "\x00".b)
  encrypted_verifier_hash = aes_ecb_encrypt(k, padded_hash)

  # Build EncryptionInfo Stream
  header_size = 32 + CSP_NAME.bytesize
  info_stream = +""
  # Version & Flags: vMajor=3, vMinor=2, Flags=0x24 (CryptoAPI AES-128)
  info_stream << [3, 2, 0x24].pack("vvV")
  # EncryptionHeader
  info_stream << [header_size].pack("V")
  info_stream << [0x24, 0, 0x0000660E, 0x00008004, 128, 0x00000018, 0, 0].pack("V8")
  info_stream << CSP_NAME
  # EncryptionVerifier
  info_stream << [16].pack("V") # Salt size
  info_stream << salt
  info_stream << encrypted_verifier # 16 bytes
  info_stream << [20].pack("V") # Verifier hash size (SHA-1 = 20)
  info_stream << encrypted_verifier_hash # 32 bytes

  # Build EncryptedPackage Stream
  padded_plain = plain_bytes
  padded_plain = padded_plain.ljust(padded_plain.bytesize + 16 - (padded_plain.bytesize % 16), "\x00".b) if (padded_plain.bytesize % 16) != 0

  encrypted_pkg_data = aes_ecb_encrypt(k, padded_plain)
  pkg_stream = [plain_bytes.bytesize].pack("Q<") + encrypted_pkg_data

  {
    "EncryptionInfo" => info_stream,
    "EncryptedPackage" => pkg_stream
  }
end