lib/origami/encryption.rb in origami-2.0.0 vs lib/origami/encryption.rb in origami-2.0.1

- old
+ new

@@ -16,16 +16,11 @@ You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see <http://www.gnu.org/licenses/>. =end -begin - require 'openssl' if Origami::OPTIONS[:use_openssl] -rescue LoadError - Origami::OPTIONS[:use_openssl] = false -end - +require 'openssl' require 'securerandom' require 'digest/md5' require 'digest/sha2' module Origami @@ -53,15 +48,16 @@ # _passwd_:: The password to decrypt the document. # def decrypt(passwd = "") raise EncryptionError, "PDF is not encrypted" unless self.encrypted? - encrypt_dict = trailer_key(:Encrypt) - handler = Encryption::Standard::Dictionary.new(encrypt_dict.dup) + # Turn the encryption dictionary into a standard encryption dictionary. + handler = trailer_key(:Encrypt) + handler = self.cast_object(handler.reference, Encryption::Standard::Dictionary) unless handler.Filter == :Standard - raise EncryptionNotSupportedError, "Unknown security handler : '#{handler.Filter.to_s}'" + raise EncryptionNotSupportedError, "Unknown security handler : '#{handler.Filter}'" end crypt_filters = { Identity: Encryption::Identity } @@ -122,52 +118,18 @@ end else raise EncryptionInvalidPasswordError end - encrypt_metadata = (handler.EncryptMetadata != false) - self.extend(Encryption::EncryptedDocument) self.encryption_handler = handler self.crypt_filters = crypt_filters self.encryption_key = encryption_key self.stm_filter, self.str_filter = stream_filter, string_filter - # - # Should be fixed to exclude only the active XRefStream - # - metadata = self.Catalog.Metadata + decrypt_objects - self.indirect_objects.each do |indobj| - encrypted_objects = [] - case indobj - when String,Stream then encrypted_objects << indobj - when Dictionary,Array then encrypted_objects |= indobj.strings_cache - end - - encrypted_objects.each do |obj| - case obj - when String - next if obj.equal?(encrypt_dict[:U]) or - obj.equal?(encrypt_dict[:O]) or - obj.equal?(encrypt_dict[:UE]) or - obj.equal?(encrypt_dict[:OE]) or - obj.equal?(encrypt_dict[:Perms]) or - (obj.parent.is_a?(Signature::DigitalSignature) and - obj.equal?(obj.parent[:Contents])) - - obj.extend(Encryption::EncryptedString) unless obj.is_a?(Encryption::EncryptedString) - obj.decrypt! - - when Stream - next if obj.is_a?(XRefStream) or (not encrypt_metadata and obj.equal?(metadata)) - - obj.extend(Encryption::EncryptedStream) unless obj.is_a?(Encryption::EncryptedStream) - end - end - end - self end # # Encrypts the current document with the provided passwords. @@ -184,103 +146,150 @@ # params = { :user_passwd => '', :owner_passwd => '', - :cipher => 'rc4', # :RC4 or :AES + :cipher => 'aes', # :RC4 or :AES :key_size => 128, # Key size in bits :hardened => false, # Use newer password validation (since Reader X) :encrypt_metadata => true, # Metadata shall be encrypted? :permissions => Encryption::Standard::Permissions::ALL # Document permissions }.update(options) - userpasswd, ownerpasswd = params[:user_passwd], params[:owner_passwd] + # Get the cryptographic parameters. + version, revision, crypt_filters = crypto_revision_from_options(params) - case params[:cipher].upcase - when 'RC4' - algorithm = Encryption::RC4 - if (40..128) === params[:key_size] and params[:key_size] % 8 == 0 - if params[:key_size] > 40 - version = 2 - revision = 3 - else - version = 1 - revision = 2 - end - else - raise EncryptionError, "Invalid RC4 key length" - end + # Create the security handler. + handler, encryption_key = create_security_handler(version, revision, params) - crypt_filters = Hash.new(algorithm) - string_filter = stream_filter = nil + # Turn this document into an EncryptedDocument instance. + self.extend(Encryption::EncryptedDocument) + self.encryption_handler = handler + self.encryption_key = encryption_key + self.crypt_filters = crypt_filters + self.stm_filter = self.str_filter = :StdCF - when 'AES' - algorithm = Encryption::AES - if params[:key_size] == 128 - version = revision = 4 - elsif params[:key_size] == 256 - version = 5 - if params[:hardened] - revision = 6 - else - revision = 5 - end - else - raise EncryptionError, "Invalid AES key length (Only 128 and 256 bits keys are supported)" - end + self + end - crypt_filters = { - Identity: Encryption::Identity, - StdCF: algorithm - } - string_filter = stream_filter = :StdCF + private - else - raise EncryptionNotSupportedError, "Cipher not supported : #{params[:cipher]}" - end + # + # Installs the standard security dictionary, marking the document as being encrypted. + # Returns the handler and the encryption key used for protecting contents. + # + def create_security_handler(version, revision, params) + # Ensure the document has an ID. doc_id = (trailer_key(:ID) || generate_id).first + # Create the standard encryption dictionary. handler = Encryption::Standard::Dictionary.new - handler.Filter = :Standard #:nodoc: + handler.Filter = :Standard handler.V = version handler.R = revision handler.Length = params[:key_size] handler.P = -1 # params[:Permissions] + # Build the crypt filter dictionary. if revision >= 4 handler.EncryptMetadata = params[:encrypt_metadata] handler.CF = Dictionary.new - cryptfilter = Encryption::CryptFilterDictionary.new - cryptfilter.AuthEvent = :DocOpen + crypt_filter = Encryption::CryptFilterDictionary.new + crypt_filter.AuthEvent = :DocOpen if revision == 4 - cryptfilter.CFM = :AESV2 + crypt_filter.CFM = :AESV2 else - cryptfilter.CFM = :AESV3 + crypt_filter.CFM = :AESV3 end - cryptfilter.Length = params[:key_size] >> 3 + crypt_filter.Length = params[:key_size] >> 3 - handler.CF[:StdCF] = cryptfilter + handler.CF[:StdCF] = crypt_filter handler.StmF = handler.StrF = :StdCF end - handler.set_passwords(ownerpasswd, userpasswd, doc_id) - encryption_key = handler.compute_user_encryption_key(userpasswd, doc_id) + user_passwd, owner_passwd = params[:user_passwd], params[:owner_passwd] - file_info = get_trailer_info - file_info[:Encrypt] = self << handler + # Setup keys. + handler.set_passwords(owner_passwd, user_passwd, doc_id) + encryption_key = handler.compute_user_encryption_key(user_passwd, doc_id) - self.extend(Encryption::EncryptedDocument) - self.encryption_handler = handler - self.encryption_key = encryption_key - self.crypt_filters = crypt_filters - self.stm_filter = self.str_filter = :StdCF + # Install the encryption dictionary to the document. + self.trailer.Encrypt = self << handler - self + [ handler, encryption_key ] end + + # + # Converts the parameters passed to PDF#encrypt. + # Returns [ version, revision, crypt_filters ] + # + def crypto_revision_from_options(params) + case params[:cipher].upcase + when 'RC4' + algorithm = Encryption::RC4 + version, revision = crypto_revision_from_rc4_key(params[:key_size]) + crypt_filters = Hash.new(algorithm) + + when 'AES' + algorithm = Encryption::AES + version, revision = crypto_revision_from_aes_key(params[:key_size], params[:hardened]) + + crypt_filters = { + Identity: Encryption::Identity, + StdCF: algorithm + } + else + raise EncryptionNotSupportedError, "Cipher not supported : #{params[:cipher]}" + end + + [ version, revision, crypt_filters ] + end + + # + # Compute the required standard security handler version based on the RC4 key size. + # _key_size_:: Key size in bits. + # Returns [ version, revision ]. + # + def crypto_revision_from_rc4_key(key_size) + raise EncryptionError, "Invalid RC4 key length" unless (40..128) === key_size and key_size % 8 == 0 + + if key_size > 40 + version = 2 + revision = 3 + else + version = 1 + revision = 2 + end + + [ version, revision ] + end + + # + # Compute the required standard security handler version based on the AES key size. + # _key_size_:: Key size in bits. + # _hardened_:: Use the extension level 8 hardened derivation algorithm. + # Returns [ version, revision ]. + # + def crypto_revision_from_aes_key(key_size, hardened) + if key_size == 128 + version = revision = 4 + elsif key_size == 256 + version = 5 + if hardened + revision = 6 + else + revision = 5 + end + else + raise EncryptionError, "Invalid AES key length (Only 128 and 256 bits keys are supported)" + end + + [ version, revision ] + end end # # Module to provide support for encrypting and decrypting PDF documents. # @@ -295,15 +304,11 @@ # # Generates _n_ random bytes from a crypto PRNG. # def self.strong_rand_bytes(n) - if Origami::OPTIONS[:use_openssl] - OpenSSL::Random.random_bytes(n) - else - SecureRandom.random_bytes(n) - end + SecureRandom.random_bytes(n) end module EncryptedDocument attr_accessor :encryption_key attr_accessor :encryption_handler @@ -325,94 +330,95 @@ encryption_cipher @stm_filter end private - def physicalize(options = {}) + # + # For each object subject to encryption, convert it to an EncryptedObject and decrypt it if necessary. + # + def decrypt_objects + each_encryptable_object do |object| + case object + when String + object.extend(EncryptedString) unless object.is_a?(EncryptedString) + object.decrypt! - build = -> (obj, revision) do - if obj.is_a?(EncryptedObject) - if options[:decrypt] == true - obj.pre_build - obj.decrypt! - obj.decrypted = false # makes it believe no encryption pass is required - obj.post_build - - return - end + when Stream + object.extend(EncryptedStream) unless object.is_a?(EncryptedStream) end + end + end - if obj.is_a?(ObjectStream) - obj.each do |subobj| - build.call(subobj, revision) - end - end - - obj.pre_build - - case obj + # + # For each object subject to encryption, convert it to an EncryptedObject and mark it as not encrypted yet. + # + def encrypt_objects + each_encryptable_object do |object| + case object when String - if not obj.equal?(@encryption_handler[:U]) and - not obj.equal?(@encryption_handler[:O]) and - not obj.equal?(@encryption_handler[:UE]) and - not obj.equal?(@encryption_handler[:OE]) and - not obj.equal?(@encryption_handler[:Perms]) and - not (obj.parent.is_a?(Signature::DigitalSignature) and - obj.equal?(obj.parent[:Contents])) and - not obj.indirect_parent.parent.is_a?(ObjectStream) - - unless obj.is_a?(EncryptedString) - obj.extend(EncryptedString) - obj.decrypted = true - end + unless object.is_a?(EncryptedString) + object.extend(EncryptedString) + object.decrypted = true end when Stream - return if obj.is_a?(XRefStream) - return if obj.equal?(self.Catalog.Metadata) and not @encryption_handler.EncryptMetadata - - unless obj.is_a?(EncryptedStream) - obj.extend(EncryptedStream) - obj.decrypted = true + unless object.is_a?(EncryptedStream) + object.extend(EncryptedStream) + object.decrypted = true end + end + end + end - when Dictionary, Array - obj.map! do |subobj| - if subobj.indirect? - if get_object(subobj.reference) - subobj.reference - else - ref = add_to_revision(subobj, revision) - build.call(subobj, revision) - ref - end - else - subobj - end - end + # + # Iterates over each encryptable objects in the document. + # + def each_encryptable_object(&b) - obj.each do |subobj| - build.call(subobj, revision) + # Metadata may not be encrypted depending on the security handler configuration. + encrypt_metadata = (@encryption_handler.EncryptMetadata != false) + metadata = self.Catalog.Metadata + + self.each_object(recursive: true) + .lazy + .select { |object| + case object + when Stream + not object.is_a?(XRefStream) or (encrypt_metadata and object.equal?(metadata)) + when String + not object.parent.equal?(@encryption_handler) end - end + } + .each(&b) + end - obj.post_build - end + def physicalize(options = {}) + encrypt_objects - # stack up every root objects - indirect_objects_by_rev.each do |obj, revision| - build.call(obj, revision) - end + super # remove encrypt dictionary if requested if options[:decrypt] - delete_object(get_trailer_info[:Encrypt]) - get_trailer_info[:Encrypt] = nil + delete_object(self.trailer[:Encrypt]) + self.trailer[:Encrypt] = nil end self end + + def build_object(object, revision, options) + if object.is_a?(EncryptedObject) and options[:decrypt] + object.pre_build + object.decrypt! + object.decrypted = false # makes it believe no encryption pass is required + object.post_build + + return + end + + super + end end # # Module for encrypted PDF objects. # @@ -436,11 +442,11 @@ if doc.encryption_handler.V < 5 parent = self.indirect_parent no, gen = parent.no, parent.generation k = encryption_key + [no].pack("I")[0..2] + [gen].pack("I")[0..1] - key_len = (k.length > 16) ? 16 : k.length + key_len = [k.length, 16].min k << "sAlT" if cipher == Encryption::AES Digest::MD5.digest(k)[0, key_len] else encryption_key @@ -510,25 +516,11 @@ def encrypt! return self unless @decrypted encode! - if self.filters.first == :Crypt - params = decode_params.first - - if params.is_a?(Dictionary) and params.Name.is_a?(Name) - crypt_filter = params.Name.value - else - crypt_filter = :Identity - end - - cipher = self.document.encryption_cipher(crypt_filter) - else - cipher = self.document.stream_encryption_cipher - end - raise EncryptionError, "Cannot find stream encryption filter" if cipher.nil? - + cipher = get_encryption_cipher key = compute_object_key(cipher) @encoded_data = if cipher == RC4 or cipher == Identity cipher.encrypt(key, self.encoded_data) @@ -546,10 +538,26 @@ end def decrypt! return self if @decrypted + cipher = get_encryption_cipher + key = compute_object_key(cipher) + + self.encoded_data = cipher.decrypt(key, @encoded_data) + @decrypted = true + + self + end + + private + + # + # Get the stream encryption cipher. + # The cipher used may depend on the presence of a Crypt filter. + # + def get_encryption_cipher if self.filters.first == :Crypt params = decode_params.first if params.is_a?(Dictionary) and params.Name.is_a?(Name) crypt_filter = params.Name.value @@ -559,36 +567,32 @@ cipher = self.document.encryption_cipher(crypt_filter) else cipher = self.document.stream_encryption_cipher end + raise EncryptionError, "Cannot find stream encryption filter" if cipher.nil? - key = compute_object_key(cipher) - - self.encoded_data = cipher.decrypt(key, @encoded_data) - @decrypted = true - - self + cipher end end # # Identity transformation. # module Identity - def Identity.encrypt(key, data) + def Identity.encrypt(_key, data) data end - def Identity.decrypt(key, data) + def Identity.decrypt(_key, data) data end end # - # Pure Ruby implementation of the RC4 symmetric algorithm + # Class wrapper for the RC4 algorithm. # class RC4 # # Encrypts data using the given key @@ -606,145 +610,36 @@ # # Creates and initialises a new RC4 generator using given key # def initialize(key) - if Origami::OPTIONS[:use_openssl] - @key = key - else - @state = init(key) - end + @key = key end # # Encrypt/decrypt data with the RC4 encryption algorithm # def cipher(data) - return "" if data.empty? + return '' if data.empty? - if Origami::OPTIONS[:use_openssl] - rc4 = OpenSSL::Cipher::RC4.new.encrypt - rc4.key_len = @key.length - rc4.key = @key + rc4 = OpenSSL::Cipher::RC4.new.encrypt + rc4.key_len = @key.length + rc4.key = @key - output = rc4.update(data) << rc4.final - else - output = "" - i, j = 0, 0 - data.each_byte do |byte| - i = i.succ & 0xFF - j = (j + @state[i]) & 0xFF - - @state[i], @state[j] = @state[j], @state[i] - - output << (@state[@state[i] + @state[j] & 0xFF] ^ byte).chr - end - end - - output + rc4.update(data) + rc4.final end alias encrypt cipher alias decrypt cipher - - private - - def init(key) #:nodoc: - state = (0..255).to_a - - j = 0 - 256.times do |i| - j = ( j + state[i] + key[i % key.size].ord ) & 0xFF - state[i], state[j] = state[j], state[i] - end - - state - end end # - # Pure Ruby implementation of the AES symmetric algorithm. - # Using mode CBC. + # Class wrapper for AES mode CBC. # class AES - NROWS = 4 - NCOLS = 4 - BLOCKSIZE = NROWS * NCOLS + BLOCKSIZE = 16 - ROUNDS = - { - 16 => 10, - 24 => 12, - 32 => 14 - } - - # - # Rijndael S-box - # - SBOX = - [ - 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, - 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, - 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, - 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, - 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, - 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, - 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, - 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, - 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, - 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, - 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, - 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, - 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, - 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, - 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, - 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 - ] - - # - # Inverse of the Rijndael S-box - # - RSBOX = - [ - 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, - 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, - 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, - 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, - 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, - 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, - 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, - 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, - 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, - 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, - 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, - 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, - 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, - 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, - 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, - 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d - ] - - RCON = - [ - 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, - 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, - 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, - 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, - 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, - 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, - 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, - 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, - 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, - 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, - 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, - 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, - 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, - 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, - 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, - 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb - ] - attr_writer :iv def AES.encrypt(key, iv, data) AES.new(key, iv).encrypt(data) end @@ -752,11 +647,11 @@ def AES.decrypt(key, data) AES.new(key, nil).decrypt(data) end def initialize(key, iv, use_padding = true) - unless key.size == 16 or key.size == 24 or key.size == 32 + unless [16, 24, 32].include?(key.size) raise EncryptionError, "Key must have a length of 128, 192 or 256 bits." end if not iv.nil? and iv.size != BLOCKSIZE raise EncryptionError, "Initialization vector must have a length of #{BLOCKSIZE} bytes." @@ -775,79 +670,32 @@ if @use_padding padlen = BLOCKSIZE - (data.size % BLOCKSIZE) data << (padlen.chr * padlen) end - if Origami::OPTIONS[:use_openssl] - aes = OpenSSL::Cipher::Cipher.new("aes-#{@key.length << 3}-cbc").encrypt - aes.iv = @iv - aes.key = @key - aes.padding = 0 + aes = OpenSSL::Cipher.new("aes-#{@key.length << 3}-cbc").encrypt + aes.iv = @iv + aes.key = @key + aes.padding = 0 - @iv + aes.update(data) + aes.final - else - cipher = [] - cipherblock = [] - nblocks = data.size / BLOCKSIZE - - first_round = true - nblocks.times do |n| - plainblock = data[n * BLOCKSIZE, BLOCKSIZE].unpack("C*") - - if first_round - BLOCKSIZE.times do |i| plainblock[i] ^= @iv[i].ord end - else - BLOCKSIZE.times do |i| plainblock[i] ^= cipherblock[i] end - end - - first_round = false - cipherblock = aes_encrypt(plainblock) - cipher.concat(cipherblock) - end - - @iv + cipher.pack("C*") - end + @iv + aes.update(data) + aes.final end def decrypt(data) unless data.size % BLOCKSIZE == 0 raise EncryptionError, "Data must be 16-bytes padded (data size = #{data.size} bytes)" end @iv = data.slice!(0, BLOCKSIZE) - if Origami::OPTIONS[:use_openssl] - aes = OpenSSL::Cipher::Cipher.new("aes-#{@key.length << 3}-cbc").decrypt - aes.iv = @iv - aes.key = @key - aes.padding = 0 + aes = OpenSSL::Cipher.new("aes-#{@key.length << 3}-cbc").decrypt + aes.iv = @iv + aes.key = @key + aes.padding = 0 - plain = (aes.update(data) + aes.final).unpack("C*") - else - plain = [] - plainblock = [] - prev_cipherblock = [] - nblocks = data.size / BLOCKSIZE + plain = (aes.update(data) + aes.final).unpack("C*") - first_round = true - nblocks.times do |n| - cipherblock = data[n * BLOCKSIZE, BLOCKSIZE].unpack("C*") - - plainblock = aes_decrypt(cipherblock) - - if first_round - BLOCKSIZE.times do |i| plainblock[i] ^= @iv[i].ord end - else - BLOCKSIZE.times do |i| plainblock[i] ^= prev_cipherblock[i] end - end - - first_round = false - prev_cipherblock = cipherblock - plain.concat(plainblock) - end - end - if @use_padding padlen = plain[-1] unless (1..16) === padlen raise EncryptionError, "Incorrect padding length : #{padlen}" end @@ -858,227 +706,10 @@ end end plain.pack("C*") end - - private - - def rol(row, n = 1) #:nodoc - n.times do row.push row.shift end ; row - end - - def ror(row, n = 1) #:nodoc: - n.times do row.unshift row.pop end ; row - end - - def galois_mult(a, b) #:nodoc: - p = 0 - - 8.times do - p ^= a if b[0] == 1 - highBit = a[7] - a <<= 1 - a ^= 0x1b if highBit == 1 - b >>= 1 - end - - p % 256 - end - - def schedule_core(word, iter) #:nodoc: - rol(word) - word.map! do |byte| SBOX[byte] end - word[0] ^= RCON[iter] - - word - end - - def transpose(m) #:nodoc: - [ - m[NROWS * 0, NROWS], - m[NROWS * 1, NROWS], - m[NROWS * 2, NROWS], - m[NROWS * 3, NROWS] - ].transpose.flatten - end - - # - # AES round methods. - # - - def create_round_key(expanded_key, round = 0) #:nodoc: - transpose(expanded_key[round * BLOCKSIZE, BLOCKSIZE]) - end - - def add_round_key(roundKey) #:nodoc: - BLOCKSIZE.times do |i| @state[i] ^= roundKey[i] end - end - - def sub_bytes #:nodoc: - BLOCKSIZE.times do |i| @state[i] = SBOX[ @state[i] ] end - end - - def r_sub_bytes #:nodoc: - BLOCKSIZE.times do |i| @state[i] = RSBOX[ @state[i] ] end - end - - def shift_rows #:nodoc: - NROWS.times do |i| - @state[i * NCOLS, NCOLS] = rol(@state[i * NCOLS, NCOLS], i) - end - end - - def r_shift_rows #:nodoc: - NROWS.times do |i| - @state[i * NCOLS, NCOLS] = ror(@state[i * NCOLS, NCOLS], i) - end - end - - def mix_column_with_field(column, field) #:nodoc: - p = field - - column[0], column[1], column[2], column[3] = - galois_mult(column[0], p[0]) ^ - galois_mult(column[3], p[1]) ^ - galois_mult(column[2], p[2]) ^ - galois_mult(column[1], p[3]), - - galois_mult(column[1], p[0]) ^ - galois_mult(column[0], p[1]) ^ - galois_mult(column[3], p[2]) ^ - galois_mult(column[2], p[3]), - - galois_mult(column[2], p[0]) ^ - galois_mult(column[1], p[1]) ^ - galois_mult(column[0], p[2]) ^ - galois_mult(column[3], p[3]), - - galois_mult(column[3], p[0]) ^ - galois_mult(column[2], p[1]) ^ - galois_mult(column[1], p[2]) ^ - galois_mult(column[0], p[3]) - end - - def mix_column(column) #:nodoc: - mix_column_with_field(column, [ 2, 1, 1, 3 ]) - end - - def r_mix_column_(column) #:nodoc: - mix_column_with_field(column, [ 14, 9, 13, 11 ]) - end - - def mix_columns #:nodoc: - NCOLS.times do |c| - column = [] - NROWS.times do |r| column << @state[c + r * NCOLS] end - mix_column(column) - NROWS.times do |r| @state[c + r * NCOLS] = column[r] end - end - end - - def r_mix_columns #:nodoc: - NCOLS.times do |c| - column = [] - NROWS.times do |r| column << @state[c + r * NCOLS] end - r_mix_column_(column) - NROWS.times do |r| @state[c + r * NCOLS] = column[r] end - end - end - - def expand_key(key) #:nodoc: - key = key.unpack("C*") - size = key.size - expanded_size = 16 * (ROUNDS[key.size] + 1) - rcon_iter = 1 - expanded_key = key[0, size] - - while expanded_key.size < expanded_size - temp = expanded_key[-4, 4] - - if expanded_key.size % size == 0 - schedule_core(temp, rcon_iter) - rcon_iter = rcon_iter.succ - end - - temp.map! do |b| SBOX[b] end if size == 32 and expanded_key.size % size == 16 - - temp.each do |b| expanded_key << (expanded_key[-size] ^ b) end - end - - expanded_key - end - - def aes_round(round_key) #:nodoc: - sub_bytes - #puts "after sub_bytes: #{@state.inspect}" - shift_rows - #puts "after shift_rows: #{@state.inspect}" - mix_columns - #puts "after mix_columns: #{@state.inspect}" - add_round_key(round_key) - #puts "roundKey = #{roundKey.inspect}" - #puts "after add_round_key: #{@state.inspect}" - end - - def r_aes_round(round_key) #:nodoc: - add_round_key(round_key) - r_mix_columns - r_shift_rows - r_sub_bytes - end - - def aes_encrypt(block) #:nodoc: - @state = transpose(block) - expanded_key = expand_key(@key) - rounds = ROUNDS[@key.size] - - aes_main(expanded_key, rounds) - end - - def aes_decrypt(block) #:nodoc: - @state = transpose(block) - expanded_key = expand_key(@key) - rounds = ROUNDS[@key.size] - - r_aes_main(expanded_key, rounds) - end - - def aes_main(expanded_key, rounds) #:nodoc: - #puts "expandedKey: #{expandedKey.inspect}" - round_key = create_round_key(expanded_key) - add_round_key(round_key) - - for i in 1..rounds-1 - round_key = create_round_key(expanded_key, i) - aes_round(round_key) - end - - round_key = create_round_key(expanded_key, rounds) - sub_bytes - shift_rows - add_round_key(round_key) - - transpose(@state) - end - - def r_aes_main(expanded_key, rounds) #:nodoc: - round_key = create_round_key(expanded_key, rounds) - add_round_key(round_key) - r_shift_rows - r_sub_bytes - - (rounds - 1).downto(1) do |i| - round_key = create_round_key(expanded_key, i) - r_aes_round(round_key) - end - - round_key = create_round_key(expanded_key) - add_round_key(round_key) - - transpose(@state) - end end # # Class representing a crypt filter Dictionary # @@ -1154,131 +785,138 @@ end end # # Computes the key that will be used to encrypt/decrypt the document contents with user password. + # Called at all revisions. # - def compute_user_encryption_key(userpassword, fileid) - if self.R < 5 - padded = pad_password(userpassword) - padded.force_encoding('binary') + def compute_user_encryption_key(user_password, file_id) + return compute_legacy_user_encryption_key(user_password, file_id) if self.R < 5 - padded << self.O - padded << [ self.P ].pack("i") + passwd = password_to_utf8(user_password) - padded << fileid + uks = self.U[40, 8] - encrypt_metadata = self.EncryptMetadata != false - padded << [ -1 ].pack("i") if self.R >= 4 and not encrypt_metadata + if self.R == 5 + ukey = Digest::SHA256.digest(passwd + uks) + else + ukey = compute_hardened_hash(passwd, uks) + end - key = Digest::MD5.digest(padded) + iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") + AES.new(ukey, nil, false).decrypt(iv + self.UE.value) + end - 50.times { key = Digest::MD5.digest(key[0, self.Length / 8]) } if self.R >= 3 + # + # Computes the key that will be used to encrypt/decrypt the document contents. + # Only for Revision 4 and less. + # + def compute_legacy_user_encryption_key(user_password, file_id) + padded = pad_password(user_password) + padded.force_encoding('binary') - if self.R == 2 - key[0, 5] - elsif self.R >= 3 - key[0, self.Length / 8] - end - else - passwd = password_to_utf8(userpassword) + padded << self.O + padded << [ self.P ].pack("i") - uks = self.U[40, 8] + padded << file_id - if self.R == 5 - ukey = Digest::SHA256.digest(passwd + uks) - else - ukey = compute_hardened_hash(passwd, uks) - end + encrypt_metadata = self.EncryptMetadata != false + padded << [ -1 ].pack("i") if self.R >= 4 and not encrypt_metadata - iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") - AES.new(ukey, nil, false).decrypt(iv + self.UE.value) - end + key = Digest::MD5.digest(padded) + + 50.times { key = Digest::MD5.digest(key[0, self.Length / 8]) } if self.R >= 3 + + truncate_key(key) end # # Computes the key that will be used to encrypt/decrypt the document contents with owner password. # Revision 5 and above. # - def compute_owner_encryption_key(ownerpassword) - if self.R >= 5 - passwd = password_to_utf8(ownerpassword) + def compute_owner_encryption_key(owner_password) + return if self.R < 5 - oks = self.O[40, 8] + passwd = password_to_utf8(owner_password) + oks = self.O[40, 8] - if self.R == 5 - okey = Digest::SHA256.digest(passwd + oks + self.U) - else - okey = compute_hardened_hash(passwd, oks, self.U) - end - - iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") - AES.new(okey, nil, false).decrypt(iv + self.OE.value) + if self.R == 5 + okey = Digest::SHA256.digest(passwd + oks + self.U) + else + okey = compute_hardened_hash(passwd, oks, self.U) end + + iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") + AES.new(okey, nil, false).decrypt(iv + self.OE.value) end # # Set up document passwords. # - def set_passwords(ownerpassword, userpassword, salt = nil) - if self.R < 5 - key = compute_owner_key(ownerpassword) - upadded = pad_password(userpassword) + def set_passwords(owner_password, user_password, salt = nil) + return set_legacy_passwords(owner_password, user_password, salt) if self.R < 5 - owner_key = RC4.encrypt(key, upadded) - 19.times { |i| owner_key = RC4.encrypt(xor(key,i+1), owner_key) } if self.R >= 3 + upass = password_to_utf8(user_password) + opass = password_to_utf8(owner_password) - self.O = owner_key - self.U = compute_user_password(userpassword, salt) + uvs, uks, ovs, oks = ::Array.new(4) { Encryption.rand_bytes(8) } + file_key = Encryption.strong_rand_bytes(32) + iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") + if self.R == 5 + self.U = Digest::SHA256.digest(upass + uvs) + uvs + uks + self.O = Digest::SHA256.digest(opass + ovs + self.U) + ovs + oks + ukey = Digest::SHA256.digest(upass + uks) + okey = Digest::SHA256.digest(opass + oks + self.U) else - upass = password_to_utf8(userpassword) - opass = password_to_utf8(ownerpassword) + self.U = compute_hardened_hash(upass, uvs) + uvs + uks + self.O = compute_hardened_hash(opass, ovs, self.U) + ovs + oks + ukey = compute_hardened_hash(upass, uks) + okey = compute_hardened_hash(opass, oks, self.U) + end - uvs, uks, ovs, oks = ::Array.new(4) { Encryption.rand_bytes(8) } - file_key = Encryption.strong_rand_bytes(32) - iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") + self.UE = AES.new(ukey, iv, false).encrypt(file_key)[iv.size, 32] + self.OE = AES.new(okey, iv, false).encrypt(file_key)[iv.size, 32] - if self.R == 5 - self.U = Digest::SHA256.digest(upass + uvs) + uvs + uks - self.O = Digest::SHA256.digest(opass + ovs + self.U) + ovs + oks - ukey = Digest::SHA256.digest(upass + uks) - okey = Digest::SHA256.digest(opass + oks + self.U) - else - self.U = compute_hardened_hash(upass, uvs) + uvs + uks - self.O = compute_hardened_hash(opass, ovs, self.U) + ovs + oks - ukey = compute_hardened_hash(upass, uks) - okey = compute_hardened_hash(opass, oks, self.U) - end + perms = + [ self.P ].pack("V") + # 0-3 + [ -1 ].pack("V") + # 4-7 + (self.EncryptMetadata == true ? "T" : "F") + # 8 + "adb" + # 9-11 + [ 0 ].pack("V") # 12-15 - self.UE = AES.new(ukey, iv, false).encrypt(file_key)[iv.size, 32] - self.OE = AES.new(okey, iv, false).encrypt(file_key)[iv.size, 32] + self.Perms = AES.new(file_key, iv, false).encrypt(perms)[iv.size, 16] - perms = - [ self.P ].pack("V") + # 0-3 - [ -1 ].pack("V") + # 4-7 - (self.EncryptMetadata == true ? "T" : "F") + # 8 - "adb" + # 9-11 - [ 0 ].pack("V") # 12-15 + file_key + end - self.Perms = AES.new(file_key, iv, false).encrypt(perms)[iv.size, 16] + # + # Set up document passwords. + # Only for Revision 4 and less. + # + def set_legacy_passwords(owner_password, user_password, salt) + owner_key = compute_owner_key(owner_password) + upadded = pad_password(user_password) - file_key - end + owner_key_hash = RC4.encrypt(owner_key, upadded) + 19.times { |i| owner_key_hash = RC4.encrypt(xor(owner_key, i + 1), owner_key_hash) } if self.R >= 3 + + self.O = owner_key_hash + self.U = compute_user_password_hash(user_password, salt) end # # Checks user password. - # For version 2,3 and 4, _salt_ is the document ID. + # For version 2, 3 and 4, _salt_ is the document ID. # For version 5 and 6, _salt_ is the User Key Salt. # def is_user_password?(pass, salt) if self.R == 2 - compute_user_password(pass, salt) == self.U + compute_user_password_hash(pass, salt) == self.U elsif self.R == 3 or self.R == 4 - compute_user_password(pass, salt)[0, 16] == self.U[0, 16] + compute_user_password_hash(pass, salt)[0, 16] == self.U[0, 16] elsif self.R == 5 uvs = self.U[32, 8] Digest::SHA256.digest(password_to_utf8(pass) + uvs) == self.U[0, 32] elsif self.R == 6 uvs = self.U[32, 8] @@ -1307,13 +945,13 @@ # # Retrieve user password from owner password. # Cannot be used with revision 5. # - def retrieve_user_password(ownerpassword) + def retrieve_user_password(owner_password) - key = compute_owner_key(ownerpassword) + key = compute_owner_key(owner_password) if self.R == 2 RC4.decrypt(key, self.O) elsif self.R == 3 or self.R == 4 user_password = RC4.decrypt(xor(key, 19), self.O) @@ -1328,35 +966,31 @@ # # Used to encrypt/decrypt the O field. # Rev 2,3,4: O = crypt(user_pass, owner_key). # Rev 5: unused. # - def compute_owner_key(ownerpassword) #:nodoc: + def compute_owner_key(owner_password) #:nodoc: - opadded = pad_password(ownerpassword) + opadded = pad_password(owner_password) - hash = Digest::MD5.digest(opadded) - 50.times { hash = Digest::MD5.digest(hash) } if self.R >= 3 + owner_key = Digest::MD5.digest(opadded) + 50.times { owner_key = Digest::MD5.digest(owner_key) } if self.R >= 3 - if self.R == 2 - hash[0, 5] - elsif self.R >= 3 - hash[0, self.Length / 8] - end + truncate_key(owner_key) end # # Compute the value of the U field. # Cannot be used with revision 5. # - def compute_user_password(userpassword, salt) #:nodoc: + def compute_user_password_hash(user_password, salt) #:nodoc: if self.R == 2 - key = compute_user_encryption_key(userpassword, salt) + key = compute_user_encryption_key(user_password, salt) user_key = RC4.encrypt(key, PADDING) elsif self.R == 3 or self.R == 4 - key = compute_user_encryption_key(userpassword, salt) + key = compute_user_encryption_key(user_password, salt) upadded = PADDING + salt hash = Digest::MD5.digest(upadded) user_key = RC4.encrypt(key, hash) @@ -1380,18 +1014,14 @@ i = 0 while i < 64 or i < x[-1].ord + 32 block = input[0, block_size] - if Origami::OPTIONS[:use_openssl] - aes = OpenSSL::Cipher::Cipher.new("aes-128-cbc").encrypt - aes.iv = iv - aes.key = key - aes.padding = 0 - else - fail "You need OpenSSL support to encrypt/decrypt documents with this method" - end + aes = OpenSSL::Cipher.new("aes-128-cbc").encrypt + aes.iv = iv + aes.key = key + aes.padding = 0 64.times do |j| x = '' x += aes.update(password) unless password.empty? x += aes.update(block) @@ -1414,16 +1044,28 @@ end h[0, 32] end + # + # Some revision handlers require different key sizes. + # Revision 2 uses 40-bit keys. + # Revisions 3 and higher rely on the Length field for the key size. + # + def truncate_key(key) + if self.R == 2 + key[0, 5] + elsif self.R >= 3 + key[0, self.Length / 8] + end + end + def xor(str, byte) #:nodoc: - str.split(//).map!{|c| (c[0].ord ^ byte).chr }.join + str.bytes.map!{|b| b ^ byte }.pack("C*") end def pad_password(password) #:nodoc: - return PADDING.dup if password.empty? # Fix for Ruby 1.9 bug - password[0,32].ljust(32, PADDING) + password[0, 32].ljust(32, PADDING) end def password_to_utf8(passwd) #:nodoc: LiteralString.new(passwd).to_utf8[0, 127] end