lib/axlsx/util/ms_off_crypto.rb in axlsx-1.0.14 vs lib/axlsx/util/ms_off_crypto.rb in axlsx-1.0.15

- old
+ new

@@ -1,88 +1,189 @@ +# -*- coding: utf-8 -*- require 'digest' require 'base64' require 'openssl' + module Axlsx + + # The MsOffCrypto class implements ECMA-367 encryption based on the MS-OFF-CRYPTO specification class MsOffCrypto - attr_reader :verifier - attr_reader :key + # Creates a new MsOffCrypto Object + # @param [String] file_name the location of the file you want to encrypt + # @param [String] pwd the password to use when encrypting the file + def initialize(file_name, pwd) + self.password = pwd + self.file_name = file_name + end - def initialize(password = "passowrd") - @password = password - @salt_size = 0x10 - @key_size = 0x100 - @verifier = rand(16**16).to_s + # Generates a new CBF file based on this instance of ms-off-crypto and overwrites the unencrypted file. + def save + cfb = Cbf.new(self) + cfb.save + end - #fixed salt for testing - @salt = [0x90,0xAC,0x68,0x0E,0x76,0xF9,0x43,0x2B,0x8D,0x13,0xB7,0x1D,0xB7,0xC0,0xFC,0x0D].join - # @salt =Digest::SHA1.digest(rand(16**16).to_s) + # returns the raw password used in encryption + # @return [String] + attr_reader :password + + # sets the password to be used for encryption + # @param [String] v the password, @default 'password' + # @return [String] + def password=(v) + @password = v || 'password' end - def encryption_info - # v.major v.minor flags header length flags size # AES 128 bit - header = [3, 0, 2, 0, 0x24, 0, 0, 0, 0xA4, 0, 0, 0, 0x24, 0, 0, 0, 0, 0, 0, 0, 0x0E, 0x66, 0, 0] - header.concat [0x04, 0x80, 0, 0, 0x80, 0, 0, 0, 0x18, 0, 0, 0, 0xA0, 0xC7, 0xDC, 0x2, 0, 0, 0, 0] - header.concat "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)".bytes.to_a.pack('s*').bytes.to_a - header.concat [0, 0] - header.concat [0x10, 0, 0, 0] - header.concat [0x90,0xAC,0x68,0x0E,0x76,0xF9,0x43,0x2B,0x8D,0x13,0xB7,0x1D,0xB7,0xC0,0xFC,0x0D] - header.concat encrypted_verifier.bytes.to_a.pack('c*').bytes.to_a - header.concat [20, 0,0,0] - header.concat encrypted_verifier_hash.bytes.to_a.pack('c*').bytes.to_a - header.flatten! - header.pack('c*') + # retruns the file name of the archive to be encrypted + # @return [String] + attr_reader :file_name + + # sets the filename + # @return [String] + def file_name=(v) + #TODO verfify that the file specified exists and is an unencrypted xlsx archive + @file_name = v end - def encryption_verifier - {:salt_size => @salt_size, - :salt => @salt, - :encrypted_verifier => encrypted_verifier, - :varifier_hash_size => 0x14, - :encrypted_verifier_hash => encrypted_verifier_hash} + + # encrypts and returns the package specified by the file name + # @return [String] + def encrypted_package + @encrypted_package ||= encrypt_package(file_name, password) end - # 2.3.3 + # returns the encryption info for this instance of ms-off-crypto + # @return [String] + def encryption_info + @encryption_info ||= create_encryption_info + end + + # returns a random salt + # @return [String] + def salt + @salt ||= Digest::SHA1.digest(rand(16**16).to_s) + end + + # returns a random verifier + # @return [String] + def verifier + @verifier ||= rand(16**16).to_s + end + + # returns the verifier encrytped + # @return [String] def encrypted_verifier - @encrypted_verifier ||= encrypt(@verifier) + @encrypted_verifier ||= encrypt(verifier) end - # 2.3.3 + # returns the verifier hash encrypted + # @return [String] def encrypted_verifier_hash - verifier_hash = Digest::SHA1.digest(@verifier) - verifier_hash << Array.new(32 - verifier_hash.size, 0).join('') @encrypted_verifier_hash ||= encrypt(verifier_hash) end + # returns a verifier hash + # @return [String] + def verifier_hash + @verifier_hash ||= create_verifier_hash + end + + # returns an encryption key + # @return [String] + def key + @key ||= create_key + end + + # size of unencrypted package? concated with encrypted package + def encrypt_package(file_name, password) + package = File.open(file_name, 'r') + package_text = package.read + [package_text.bytes.to_a.size].pack('q') + encrypt(package_text) + end + + # Generates an encryption info structure + # @return [String] + def create_encryption_info + header = [3, 0, 2, 0] # version + # Header flags copy + header.concat [0x24, 0, 0, 0] #flags -- VERY UNSURE ABOUT THIS STILL + header.concat [0, 0, 0, 0] #unused + header.concat [0xA4, 0, 0, 0] #length + # Header + header.concat [0x24, 0, 0, 0] #flags again + header.concat [0, 0, 0, 0] #unused again, + header.concat [0x0E, 0x66, 0, 0] #alg id + header.concat [0x04, 0x80, 0, 0] #alg hash id + header.concat [key.size, 0, 0, 0] #key size + header.concat [0x18, 0, 0, 0] #provider type + header.concat [0, 0, 0, 0] #reserved 1 + header.concat [0, 0, 0, 0] #reserved 2 + #header.concat [0xA0, 0xC7, 0xDC, 0x2, 0, 0, 0, 0] + header.concat "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)".bytes.to_a.pack('s*').bytes.to_a + header.concat [0, 0] #null terminator + + #Salt Size + header.concat [salt.bytes.to_a.size].pack('l').bytes.to_a + #Salt + header.concat salt.bytes.to_a.pack('c*').bytes.to_a + # encryption verifier + header.concat encrypted_verifier.bytes.to_a.pack('c*').bytes.to_a + + # verifier hash size -- MUST BE 32 bytes + header.concat [verifier_hash.bytes.to_a.size].pack('l').bytes.to_a + + #encryption verifier hash + header.concat encrypted_verifier_hash.bytes.to_a.pack('c*').bytes.to_a + + header.flatten! + header.pack('c*') + end + + # 2.3.3 + def create_verifier_hash + vh = Digest::SHA1.digest(verifier) + vh << Array.new(32 - vh.size, 0).join('') + end + # 2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption) - def key - sha = Digest::SHA1.new() << (@salt + @password) + def create_key + sha = Digest::SHA1.new() << (salt + @password) (0..49999).each { |i| sha.update(i.to_s+sha.to_s) } key = sha.update(sha.to_s+'0').digest a = key.bytes.each_with_index.map { |item, i| 0x36 ^ item } x1 = Digest::SHA1.digest((a.concat Array.new(64 - key.size, 0x36)).to_s) a = key.bytes.each_with_index.map { |item, i| 0x5C ^ item } x2 = Digest::SHA1.digest( (a.concat Array.new(64 - key.size, 0x5C) ).to_s) x3 = x1 + x2 - @key ||= x3.bytes.to_a[(0..31)].pack('c*') + x3.bytes.to_a[(0..31)].pack('c*') end + # ensures that the a hashed decryption of the encryption verifier matches the decrypted verifier hash. + # @return [Boolean] def verify_password - puts decrypt(@encrypted_verifier) + v = Digest::SHA1.digest decrypt(@encrypted_verifier) + vh = decrypt(@encrypted_verifier_hash) + vh[0..15] == v[0..15] end + # encrypts the data proved + # @param [String] data + # @return [String] the encrypted data def encrypt(data) aes = OpenSSL::Cipher.new("AES-128-ECB") aes.encrypt aes.key = key - aes.update(data) + aes.update(data) << aes.final end + # dencrypts the data proved + # @param [String] data + # @return [String] the dencrypted data def decrypt(data) aes = OpenSSL::Cipher.new("AES-128-ECB") aes.decrypt aes.key = key - aes.update(data) + aes.update(data) << aes.final end end end