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