Class: Xlsxrb::Ooxml::Crypto::Agile
- Inherits:
-
Object
- Object
- Xlsxrb::Ooxml::Crypto::Agile
- Defined in:
- lib/xlsxrb/ooxml/crypto/agile.rb,
sig/generated/xlsxrb/ooxml/crypto/agile.rbs
Overview
Implements Microsoft Office Agile Encryption specified in [MS-OFFCRYPTO] Section 2.3.4.
Constant Summary collapse
- BLOCK_KEY_VERIFIER_INPUT =
"\xfe\xa7\xd2\x76\x3b\x4b\x9e\x79".b.freeze
- BLOCK_KEY_VERIFIER_VALUE =
"\xd7\xaa\x0f\x6d\x30\x61\x34\x4e".b.freeze
- BLOCK_KEY_KEY =
"\x14\x6e\x0b\xe7\xab\xac\xd0\xd6".b.freeze
- BLOCK_KEY_INTEGRITY_KEY =
"\x5f\xb2\xad\x01\x0c\xb9\xe1\xf6".b.freeze
- BLOCK_KEY_INTEGRITY_VALUE =
"\xa0\x67\x7f\x02\xb2\x2c\x84\x33".b.freeze
- SEGMENT_SIZE =
4096
Class Method Summary collapse
-
.decrypt(encryption_info_bytes, encrypted_package_bytes, password) ⇒ Object
Decrypts an encrypted package given the EncryptionInfo stream data and EncryptedPackage stream data.
-
.encrypt(plain_bytes, password) ⇒ Object
Encrypts a plain zip payload with the given password into EncryptionInfo and EncryptedPackage streams.
Class Method Details
.decrypt(encryption_info_bytes, encrypted_package_bytes, password) ⇒ Object
Decrypts an encrypted package given the EncryptionInfo stream data and EncryptedPackage stream data.
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 |
# File 'lib/xlsxrb/ooxml/crypto/agile.rb', line 24 def decrypt(encryption_info_bytes, encrypted_package_bytes, password) info = parse_encryption_info(encryption_info_bytes) password_str = password.to_s digest = digest_for(info[:hash_algorithm]) cipher_name = cipher_name_for(info[:cipher_algorithm], info[:key_bits]) # 1. Derive intermediate hash H_final from password and keyEncryptor salt h_final = derive_h_final(digest, password_str, info[:encryptor_salt], info[:spin_count]) # 2. Verify password with verifier input and hash key_bytes = info[:key_bits] / 8 k_ver_input = derive_key(digest, h_final, BLOCK_KEY_VERIFIER_INPUT, key_bytes) verifier_input = aes_decrypt(cipher_name, k_ver_input, info[:encryptor_salt], info[:encrypted_verifier_hash_input]) expected_hash = digest.digest(verifier_input) k_ver_val = derive_key(digest, h_final, BLOCK_KEY_VERIFIER_VALUE, key_bytes) decrypted_hash = aes_decrypt(cipher_name, k_ver_val, info[:encryptor_salt], info[:encrypted_verifier_hash_value]) hash_size = info[:hash_size] raise Xlsxrb::InvalidPasswordError, "Password verification failed" unless OpenSSL.secure_compare(decrypted_hash[0, hash_size], expected_hash[0, hash_size]) # 3. Decrypt package master key k_key = derive_key(digest, h_final, BLOCK_KEY_KEY, key_bytes) package_key = aes_decrypt(cipher_name, k_key, info[:encryptor_salt], info[:encrypted_key_value])[0, key_bytes] # 4. 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 pkg_cipher_name = cipher_name_for(info[:key_data_cipher_algorithm], info[:key_data_key_bits]) pkg_digest = digest_for(info[:key_data_hash_algorithm]) pkg_salt = info[:key_data_salt] decrypted = +"" num_blocks = (encrypted_data.bytesize + SEGMENT_SIZE - 1) / SEGMENT_SIZE num_blocks.times do |i| chunk = encrypted_data[i * SEGMENT_SIZE, SEGMENT_SIZE] break if chunk.nil? || chunk.empty? iv = pkg_digest.digest(pkg_salt + [i].pack("V"))[0, 16] decrypted << aes_decrypt_block(pkg_cipher_name, package_key, iv, chunk) end decrypted[0, total_size] || "".b end |
.encrypt(plain_bytes, password) ⇒ Object
Encrypts a plain zip payload with the given password into EncryptionInfo and EncryptedPackage streams.
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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/xlsxrb/ooxml/crypto/agile.rb', line 77 def encrypt(plain_bytes, password) password_str = password.to_s spin_count = 100_000 key_bits = 256 key_bytes = key_bits / 8 hash_algorithm = "SHA512" cipher_algorithm = "AES" cipher_chaining = "ChainingModeCBC" hash_size = 64 block_size = 16 digest = digest_for(hash_algorithm) cipher_name = "aes-256-cbc" encryptor_salt = SecureRandom.random_bytes(16) key_data_salt = SecureRandom.random_bytes(16) package_key = SecureRandom.random_bytes(key_bytes) verifier_input = SecureRandom.random_bytes(16) # Derive intermediate hash H_final h_final = derive_h_final(digest, password_str, encryptor_salt, spin_count) # Encrypt verifier input k_ver_input = derive_key(digest, h_final, BLOCK_KEY_VERIFIER_INPUT, key_bytes) encrypted_verifier_input = aes_encrypt(cipher_name, k_ver_input, encryptor_salt, verifier_input) # Encrypt verifier hash value expected_hash = digest.digest(verifier_input) k_ver_val = derive_key(digest, h_final, BLOCK_KEY_VERIFIER_VALUE, key_bytes) encrypted_verifier_val = aes_encrypt(cipher_name, k_ver_val, encryptor_salt, expected_hash) # Encrypt package master key k_key = derive_key(digest, h_final, BLOCK_KEY_KEY, key_bytes) encrypted_key_value = aes_encrypt(cipher_name, k_key, encryptor_salt, package_key) # Encrypt plain package data in 4096-byte segments encrypted_package_stream = +"" encrypted_package_stream << [plain_bytes.bytesize].pack("Q<") num_blocks = (plain_bytes.bytesize + SEGMENT_SIZE - 1) / SEGMENT_SIZE num_blocks.times do |i| chunk = plain_bytes[i * SEGMENT_SIZE, SEGMENT_SIZE] || "".b # Pad last chunk to multiple of block_size if necessary if (chunk.bytesize % block_size) != 0 pad_len = block_size - (chunk.bytesize % block_size) chunk += ("\x00".b * pad_len) end iv = digest.digest(key_data_salt + [i].pack("V"))[0, 16] encrypted_package_stream << aes_encrypt_block(cipher_name, package_key, iv, chunk) end # Data integrity HMAC hmac_key = SecureRandom.random_bytes(64) hmac_value = OpenSSL::HMAC.digest(hash_algorithm, hmac_key, plain_bytes) k_integ_key = derive_key(digest, h_final, BLOCK_KEY_INTEGRITY_KEY, key_bytes) encrypted_hmac_key = aes_encrypt(cipher_name, k_integ_key, encryptor_salt, hmac_key) k_integ_val = derive_key(digest, h_final, BLOCK_KEY_INTEGRITY_VALUE, key_bytes) encrypted_hmac_val = aes_encrypt(cipher_name, k_integ_val, encryptor_salt, hmac_value) # Build EncryptionInfo XML xml = <<~XML <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <encryption xmlns="http://schemas.microsoft.com/office/2006/encryption" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate"> <keyData saltSize="#{key_data_salt.bytesize}" blockSize="#{block_size}" keyBits="#{key_bits}" hashSize="#{hash_size}" cipherAlgorithm="#{cipher_algorithm}" cipherChaining="#{cipher_chaining}" hashAlgorithm="#{hash_algorithm}" saltValue="#{[key_data_salt].pack("m0")}"/> <dataIntegrity encryptedHmacKey="#{[encrypted_hmac_key].pack("m0")}" encryptedHmacValue="#{[encrypted_hmac_val].pack("m0")}"/> <keyEncryptors> <keyEncryptor uri="http://schemas.microsoft.com/office/2006/keyEncryptor/password"> <p:encryptedKey spinCount="#{spin_count}" saltSize="#{encryptor_salt.bytesize}" blockSize="#{block_size}" keyBits="#{key_bits}" hashSize="#{hash_size}" cipherAlgorithm="#{cipher_algorithm}" cipherChaining="#{cipher_chaining}" hashAlgorithm="#{hash_algorithm}" saltValue="#{[encryptor_salt].pack("m0")}" encryptedVerifierHashInput="#{[encrypted_verifier_input].pack("m0")}" encryptedVerifierHashValue="#{[encrypted_verifier_val].pack("m0")}" encryptedKeyValue="#{[encrypted_key_value].pack("m0")}"/> </keyEncryptor> </keyEncryptors> </encryption> XML # EncryptionInfo stream header: versionMajor=4, versionMinor=4, flags=0x40 info_stream = [4, 4, 0x40].pack("vvV") + xml.b { "EncryptionInfo" => info_stream, "EncryptedPackage" => encrypted_package_stream } end |