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

- old
+ new

@@ -1,1404 +1,1435 @@ =begin -= File - encryption.rb + This file is part of Origami, PDF manipulation framework for Ruby + Copyright (C) 2016 Guillaume Delugré. -= Info - This file is part of Origami, PDF manipulation framework for Ruby - Copyright (C) 2010 Guillaume DelugrÈ <guillaume AT security-labs DOT org> - All right reserved. - - Origami is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + Origami is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. - Origami is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Lesser General Public License for more details. + Origami is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. - You should have received a copy of the GNU Lesser General Public License - along with Origami. If not, see <http://www.gnu.org/licenses/>. + 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] + require 'openssl' if Origami::OPTIONS[:use_openssl] rescue LoadError - Origami::OPTIONS[:use_openssl] = false + Origami::OPTIONS[:use_openssl] = false end +require 'securerandom' require 'digest/md5' require 'digest/sha2' module Origami - class EncryptionError < Exception #:nodoc: - end + class EncryptionError < Error #:nodoc: + end - class EncryptionInvalidPasswordError < EncryptionError #:nodoc: - end + class EncryptionInvalidPasswordError < EncryptionError #:nodoc: + end - class EncryptionNotSupportedError < EncryptionError #:nodoc: - end - - class PDF - - # - # Returns whether the PDF file is encrypted. - # - def is_encrypted? - has_attr? :Encrypt + class EncryptionNotSupportedError < EncryptionError #:nodoc: end - - # - # Decrypts the current document (only RC4 40..128 bits). - # _passwd_:: The password to decrypt the document. - # - def decrypt(passwd = "") - - unless self.is_encrypted? - raise EncryptionError, "PDF is not encrypted" - end - - encrypt_dict = get_doc_attr(:Encrypt) - handler = Encryption::Standard::Dictionary.new(encrypt_dict.dup) - unless handler.Filter == :Standard - raise EncryptionNotSupportedError, "Unknown security handler : '#{handler.Filter.to_s}'" - end + class PDF - case handler.V.to_i - when 1,2 then str_algo = stm_algo = Encryption::ARC4 - when 4,5 - if handler[:CF].is_a?(Dictionary) - cfs = handler[:CF] - - if handler[:StrF].is_a?(Name) and cfs[handler[:StrF]].is_a?(Dictionary) - cfdict = cfs[handler[:StrF]] - - str_algo = - if cfdict[:CFM] == :V2 then Encryption::ARC4 - elsif cfdict[:CFM] == :AESV2 then Encryption::AES - elsif cfdict[:CFM] == :None then Encryption::Identity - elsif cfdict[:CFM] == :AESV3 and handler.V.to_i == 5 then Encryption::AES - else - raise EncryptionNotSupportedError, "Unsupported encryption version : #{handler.V}" + # + # Returns whether the PDF file is encrypted. + # + def encrypted? + trailer_key? :Encrypt + end + + # + # Decrypts the current document (only RC4 40..128 bits). + # _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) + + unless handler.Filter == :Standard + raise EncryptionNotSupportedError, "Unknown security handler : '#{handler.Filter.to_s}'" + end + + crypt_filters = { + Identity: Encryption::Identity + } + + case handler.V.to_i + when 1,2 + crypt_filters = Hash.new(Encryption::RC4) + string_filter = stream_filter = nil + when 4,5 + crypt_filters = { + Identity: Encryption::Identity + } + + if handler[:CF].is_a?(Dictionary) + handler[:CF].each_pair do |name, cf| + next unless cf.is_a?(Dictionary) + + crypt_filters[name.value] = + if cf[:CFM] == :V2 then Encryption::RC4 + elsif cf[:CFM] == :AESV2 then Encryption::AES + elsif cf[:CFM] == :None then Encryption::Identity + elsif cf[:CFM] == :AESV3 and handler.V.to_i == 5 then Encryption::AES + else + raise EncryptionNotSupportedError, "Unsupported encryption version : #{handler.V}" + end + end end + + string_filter = handler.StrF.is_a?(Name) ? handler.StrF.value : :Identity + stream_filter = handler.StmF.is_a?(Name) ? handler.StmF.value : :Identity + + unless crypt_filters.key?(string_filter) + raise EncryptionError, "Invalid StrF value in encryption dictionary" + end + + unless crypt_filters.key?(stream_filter) + raise EncryptionError, "Invalid StmF value in encryption dictionary" + end else - str_algo = Encryption::Identity + raise EncryptionNotSupportedError, "Unsupported encryption version : #{handler.V}" end - if handler[:StmF].is_a?(Name) and cfs[handler[:StmF]].is_a?(Dictionary) - cfdict = cfs[handler[:StmF]] + doc_id = trailer_key(:ID) + unless doc_id.is_a?(Array) + raise EncryptionError, "Document ID was not found or is invalid" unless handler.V.to_i == 5 + else + doc_id = doc_id.first + end - stm_algo = - if cfdict[:CFM] == :V2 then Encryption::ARC4 - elsif cfdict[:CFM] == :AESV2 then Encryption::AES - elsif cfdict[:CFM] == :None then Encryption::Identity - elsif cfdict[:CFM] == :AESV3 and handler.V.to_i == 5 then Encryption::AES + if handler.is_user_password?(passwd, doc_id) + encryption_key = handler.compute_user_encryption_key(passwd, doc_id) + elsif handler.is_owner_password?(passwd, doc_id) + if handler.V.to_i < 5 + user_passwd = handler.retrieve_user_password(passwd) + encryption_key = handler.compute_user_encryption_key(user_passwd, doc_id) else - raise EncryptionNotSupportedError, "Unsupported encryption version : #{handler.V}" + encryption_key = handler.compute_owner_encryption_key(passwd) end else - stm_algo = Encryption::Identity + raise EncryptionInvalidPasswordError end - else - str_algo = stm_algo = Encryption::Identity - end + encrypt_metadata = (handler.EncryptMetadata != false) - else - raise EncryptionNotSupportedError, "Unsupported encryption version : #{handler.V}" - end - - doc_id = get_doc_attr(:ID) - unless doc_id.is_a?(Array) - raise EncryptionError, "Document ID was not found or is invalid" unless handler.V.to_i == 5 - else - doc_id = doc_id.first - end + 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 - if handler.is_user_password?(passwd, doc_id) - encryption_key = handler.compute_user_encryption_key(passwd, doc_id) - elsif handler.is_owner_password?(passwd, doc_id) - if handler.V.to_i < 5 - user_passwd = handler.retrieve_user_password(passwd) - encryption_key = handler.compute_user_encryption_key(user_passwd, doc_id) - else - encryption_key = handler.compute_owner_encryption_key(passwd) - end - else - raise EncryptionInvalidPasswordError - end + # + # Should be fixed to exclude only the active XRefStream + # + metadata = self.Catalog.Metadata + 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 - #self.extend(Encryption::EncryptedDocument) - #self.encryption_dict = encrypt_dict - #self.encryption_key = encryption_key - #self.stm_algo = self.str_algo = algorithm + 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])) - encrypt_metadata = (handler.EncryptMetadata != false) + obj.extend(Encryption::EncryptedString) unless obj.is_a?(Encryption::EncryptedString) + obj.decrypt! - self.extend(Encryption::EncryptedDocument) - self.encryption_dict = handler - self.encryption_key = encryption_key - self.stm_algo,self.str_algo = stm_algo,str_algo - - # - # Should be fixed to exclude only the active XRefStream - # - metadata = self.Catalog.Metadata + when Stream + next if obj.is_a?(XRefStream) or (not encrypt_metadata and obj.equal?(metadata)) - 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 + obj.extend(Encryption::EncryptedStream) unless obj.is_a?(Encryption::EncryptedStream) + end + end + end + + self end - encrypted_objects.each do |obj| + # + # Encrypts the current document with the provided passwords. + # The document will be encrypted at writing-on-disk time. + # _userpasswd_:: The user password. + # _ownerpasswd_:: The owner password. + # _options_:: A set of options to configure encryption. + # + def encrypt(options = {}) + raise EncryptionError, "PDF is already encrypted" if self.encrypted? - 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])) + # + # Default encryption options. + # + params = + { + :user_passwd => '', + :owner_passwd => '', + :cipher => 'rc4', # :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) - obj.extend(Encryption::EncryptedString) unless obj.is_a?(Encryption::EncryptedString) - obj.encryption_handler = handler - obj.encryption_key = encryption_key - obj.algorithm = str_algo - obj.decrypt! + userpasswd, ownerpasswd = params[:user_passwd], params[:owner_passwd] - 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) - obj.encryption_handler = handler - obj.encryption_key = encryption_key - obj.algorithm = stm_algo - end - end - end + 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 - self - end - - # - # Encrypts the current document with the provided passwords. - # The document will be encrypted at writing-on-disk time. - # _userpasswd_:: The user password. - # _ownerpasswd_:: The owner password. - # _options_:: A set of options to configure encryption. - # - def encrypt(options = {}) - - if self.is_encrypted? - raise EncryptionError, "PDF is already encrypted" - end + crypt_filters = Hash.new(algorithm) + string_filter = stream_filter = nil - # - # Default encryption options. - # - params = - { - :user_passwd => '', - :owner_passwd => '', - :cipher => 'rc4', # :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) + 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 - userpasswd, ownerpasswd = params[:user_passwd], params[:owner_passwd] + crypt_filters = { + Identity: Encryption::Identity, + StdCF: algorithm + } + string_filter = stream_filter = :StdCF - case params[:cipher].upcase - when 'RC4' - algorithm = Encryption::ARC4 - 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 + raise EncryptionNotSupportedError, "Cipher not supported : #{params[:cipher]}" end - else - raise EncryptionError, "Invalid RC4 key length" - end - 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 - else - raise EncryptionNotSupportedError, "Cipher not supported : #{params[:cipher]}" - end - - doc_id = (get_doc_attr(:ID) || gen_id).first - handler = Encryption::Standard::Dictionary.new - handler.Filter = :Standard #:nodoc: - handler.V = version - handler.R = revision - handler.Length = params[:key_size] - handler.P = -1 # params[:Permissions] - - if revision >= 4 - handler.EncryptMetadata = params[:encrypt_metadata] - handler.CF = Dictionary.new - cryptfilter = Encryption::CryptFilterDictionary.new - cryptfilter.AuthEvent = :DocOpen - - if revision == 4 - cryptfilter.CFM = :AESV2 - else - cryptfilter.CFM = :AESV3 - end + doc_id = (trailer_key(:ID) || generate_id).first - cryptfilter.Length = params[:key_size] >> 3 + handler = Encryption::Standard::Dictionary.new + handler.Filter = :Standard #:nodoc: + handler.V = version + handler.R = revision + handler.Length = params[:key_size] + handler.P = -1 # params[:Permissions] - handler.CF[:StdCF] = cryptfilter - handler.StmF = handler.StrF = :StdCF - end - - handler.set_passwords(ownerpasswd, userpasswd, doc_id) - encryption_key = handler.compute_user_encryption_key(userpasswd, doc_id) + if revision >= 4 + handler.EncryptMetadata = params[:encrypt_metadata] + handler.CF = Dictionary.new + cryptfilter = Encryption::CryptFilterDictionary.new + cryptfilter.AuthEvent = :DocOpen - fileInfo = get_trailer_info - fileInfo[:Encrypt] = self << handler + if revision == 4 + cryptfilter.CFM = :AESV2 + else + cryptfilter.CFM = :AESV3 + end - self.extend(Encryption::EncryptedDocument) - self.encryption_dict = handler - self.encryption_key = encryption_key - self.stm_algo = self.str_algo = algorithm + cryptfilter.Length = params[:key_size] >> 3 - self - end - - end + handler.CF[:StdCF] = cryptfilter + handler.StmF = handler.StrF = :StdCF + end - # - # Module to provide support for encrypting and decrypting PDF documents. - # - module Encryption + handler.set_passwords(ownerpasswd, userpasswd, doc_id) + encryption_key = handler.compute_user_encryption_key(userpasswd, doc_id) - # - # Generates _n_ random bytes from a fast PRNG. - # - def self.rand_bytes(n) - ::Array.new(n) { rand(256) }.pack("C*") + file_info = get_trailer_info + file_info[:Encrypt] = self << handler + + 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 + + self + end end # - # Generates _n_ random bytes from a crypto PRNG. + # Module to provide support for encrypting and decrypting PDF documents. # - def self.strong_rand_bytes(n) - if Origami::OPTIONS[:use_openssl] - OpenSSL::Random.random_bytes(n) - elsif RUBY_VERSION >= '1.9' - Random.new.bytes(n) - else - self.rand_bytes(n) - end - end + module Encryption - module EncryptedDocument + # + # Generates _n_ random bytes from a fast PRNG. + # + def self.rand_bytes(n) + Random.new.bytes(n) + end - attr_writer :encryption_key - attr_writer :encryption_dict - attr_writer :stm_algo - attr_writer :str_algo + # + # 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 + end - private + module EncryptedDocument + attr_accessor :encryption_key + attr_accessor :encryption_handler + attr_accessor :str_filter, :stm_filter + attr_accessor :crypt_filters - def physicalize(options = {}) + # Get the encryption cipher from the crypt filter name. + def encryption_cipher(name) + @crypt_filters[name] + end - def build(obj, revision, options) #:nodoc: - if obj.is_a?(EncryptedObject) # already built - if options[:decrypt] == true - obj.pre_build - obj.decrypt! - obj.decrypted = false # makes it believe no encryption pass is required - obj.post_build + # Get the default string encryption cipher. + def string_encryption_cipher + encryption_cipher @str_filter end - return - end - - if obj.is_a?(ObjectStream) - obj.each do |subobj| - build(subobj, revision, options) + # Get the default stream encryption cipher. + def stream_encryption_cipher + encryption_cipher @stm_filter end - end - obj.pre_build + private - case obj - when String - if not obj.equal?(@encryption_dict[:U]) and - not obj.equal?(@encryption_dict[:O]) and - not obj.equal?(@encryption_dict[:UE]) and - not obj.equal?(@encryption_dict[:OE]) and - not obj.equal?(@encryption_dict[:Perms]) and - not (obj.parent.is_a?(Signature::DigitalSignature) and obj.equal?(obj.parent[:Contents])) and - not obj.indirect_parent.parent.is_a?(ObjectStream) - - obj.extend(EncryptedString) - obj.decrypted = true - obj.encryption_handler = @encryption_dict - obj.encryption_key = @encryption_key - obj.algorithm = @str_algo - end + def physicalize(options = {}) - when Stream - return if obj.is_a?(XRefStream) - return if obj.equal?(self.Catalog.Metadata) and not @encryption_dict.EncryptMetadata - obj.extend(EncryptedStream) - obj.decrypted = true - obj.encryption_handler = @encryption_dict - obj.encryption_key = @encryption_key - obj.algorithm = @stm_algo + 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 - when Dictionary, Array - obj.map! do |subobj| - if subobj.is_indirect? - if get_object(subobj.reference) - subobj.reference - else - ref = add_to_revision(subobj, revision) - build(subobj, revision, options) - ref - end - else - subobj - end - end - - obj.each do |subobj| - build(subobj, revision, options) - end - end + return + end + end - obj.post_build - end - - # stack up every root objects - indirect_objects_by_rev.each do |obj, revision| - build(obj, revision, options) - end + if obj.is_a?(ObjectStream) + obj.each do |subobj| + build.call(subobj, revision) + end + end - # remove encrypt dictionary if requested - if options[:decrypt] - delete_object(get_trailer_info[:Encrypt]) - get_trailer_info[:Encrypt] = nil - end - - self - end - - end + obj.pre_build - # - # Module for encrypted PDF objects. - # - module EncryptedObject #:nodoc + case obj + 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) - attr_writer :encryption_key - attr_writer :algorithm - attr_writer :encryption_handler - attr_accessor :decrypted + unless obj.is_a?(EncryptedString) + obj.extend(EncryptedString) + obj.decrypted = true + end + end - def self.extended(obj) - obj.decrypted = false - end + when Stream + return if obj.is_a?(XRefStream) + return if obj.equal?(self.Catalog.Metadata) and not @encryption_handler.EncryptMetadata - def post_build - encrypt! + unless obj.is_a?(EncryptedStream) + obj.extend(EncryptedStream) + obj.decrypted = true + end - super - 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 - private + obj.each do |subobj| + build.call(subobj, revision) + end + end - def compute_object_key - if @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 - k << "sAlT" if @algorithm == Encryption::AES - - Digest::MD5.digest(k)[0, key_len] - else - @encryption_key + obj.post_build + end + + # stack up every root objects + indirect_objects_by_rev.each do |obj, revision| + build.call(obj, revision) + end + + # remove encrypt dictionary if requested + if options[:decrypt] + delete_object(get_trailer_info[:Encrypt]) + get_trailer_info[:Encrypt] = nil + end + + self + end end - end - end + # + # Module for encrypted PDF objects. + # + module EncryptedObject #:nodoc + attr_accessor :decrypted - # - # Module for encrypted String. - # - module EncryptedString - include EncryptedObject + def post_build + encrypt! - def encrypt! - if @decrypted - key = compute_object_key - - encrypted_data = - if @algorithm == ARC4 or @algorithm == Identity - @algorithm.encrypt(key, self.value) - else - iv = Encryption.rand_bytes(AES::BLOCKSIZE) - @algorithm.encrypt(key, iv, self.value) - end + super + end - @decrypted = false + private - self.replace(encrypted_data) - self.freeze - end - - self - end + def compute_object_key(cipher) + doc = self.document + raise EncryptionError, "Document is not encrypted" unless doc.is_a?(EncryptedDocument) - def decrypt! - unless @decrypted - key = compute_object_key - self.replace(@algorithm.decrypt(key, self.to_str)) - @decrypted = true + encryption_key = doc.encryption_key + + 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 + k << "sAlT" if cipher == Encryption::AES + + Digest::MD5.digest(k)[0, key_len] + else + encryption_key + end + end end - self - end - end + # + # Module for encrypted String. + # + module EncryptedString + include EncryptedObject - # - # Module for encrypted Stream. - # - module EncryptedStream - include EncryptedObject + def self.extended(obj) + obj.decrypted = false + end - def encrypt! - if @decrypted - encode! + def encrypt! + return self unless @decrypted - key = compute_object_key + cipher = self.document.string_encryption_cipher + raise EncryptionError, "Cannot find string encryption filter" if cipher.nil? - @rawdata = - if @algorithm == ARC4 or @algorithm == Identity - @algorithm.encrypt(key, self.rawdata) - else - iv = Encryption.rand_bytes(AES::BLOCKSIZE) - @algorithm.encrypt(key, iv, @rawdata) + key = compute_object_key(cipher) + + encrypted_data = + if cipher == RC4 or cipher == Identity + cipher.encrypt(key, self.value) + else + iv = Encryption.rand_bytes(AES::BLOCKSIZE) + cipher.encrypt(key, iv, self.value) + end + + @decrypted = false + + self.replace(encrypted_data) + self.freeze + + self end - @decrypted = false + def decrypt! + return self if @decrypted - @rawdata.freeze - self.freeze - end + cipher = self.document.string_encryption_cipher + raise EncryptionError, "Cannot find string encryption filter" if cipher.nil? - self - end + key = compute_object_key(cipher) - def decrypt! - unless @decrypted - key = compute_object_key + self.replace(cipher.decrypt(key, self.to_str)) + @decrypted = true - self.rawdata = @algorithm.decrypt(key, @rawdata) - @decrypted = true + self + end end - self - end + # + # Module for encrypted Stream. + # + module EncryptedStream + include EncryptedObject - end + def self.extended(obj) + obj.decrypted = false + end - # - # Identity transformation. - # - module Identity - def Identity.encrypt(key, data) - data - end + def encrypt! + return self unless @decrypted - def Identity.decrypt(key, data) - data - end - end + encode! - # - # Pure Ruby implementation of the aRC4 symmetric algorithm - # - class ARC4 - - # - # Encrypts data using the given key - # - def ARC4.encrypt(key, data) - ARC4.new(key).encrypt(data) - end - - # - # Decrypts data using the given key - # - def ARC4.decrypt(key, data) - ARC4.new(key).decrypt(data) - end - - # - # Creates and initialises a new aRC4 generator using given key - # - def initialize(key) - if Origami::OPTIONS[:use_openssl] - @key = key - else - @state = init(key) - end - end - - # - # Encrypt/decrypt data with the aRC4 encryption algorithm - # - def cipher(data) - return "" if data.empty? - - if Origami::OPTIONS[:use_openssl] - rc4 = OpenSSL::Cipher::RC4.new.encrypt - rc4.key_len = @key.length - rc4.key = @key + if self.filters.first == :Crypt + params = decode_params.first - 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 - 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 + if params.is_a?(Dictionary) and params.Name.is_a?(Name) + crypt_filter = params.Name.value + else + crypt_filter = :Identity + end - 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? - # - # Pure Ruby implementation of the AES symmetric algorithm. - # Using mode CBC. - # - class AES - - NROWS = 4 - NCOLS = 4 - BLOCKSIZE = NROWS * NCOLS + key = compute_object_key(cipher) - ROUNDS = - { - 16 => 10, - 24 => 12, - 32 => 14 - } + @encoded_data = + if cipher == RC4 or cipher == Identity + cipher.encrypt(key, self.encoded_data) + else + iv = Encryption.rand_bytes(AES::BLOCKSIZE) + cipher.encrypt(key, iv, @encoded_data) + end - # - # 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 - ] + @decrypted = false - # - # 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 - ] + @encoded_data.freeze + self.freeze - 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 - ] + self + end - attr_writer :iv + def decrypt! + return self if @decrypted - def AES.encrypt(key, iv, data) - AES.new(key, iv).encrypt(data) - end + if self.filters.first == :Crypt + params = decode_params.first - 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 - raise EncryptionError, "Key must have a length of 128, 192 or 256 bits." - end + if params.is_a?(Dictionary) and params.Name.is_a?(Name) + crypt_filter = params.Name.value + else + crypt_filter = :Identity + end - if not iv.nil? and iv.size != BLOCKSIZE - raise EncryptionError, "Initialization vector must have a length of #{BLOCKSIZE} bytes." - 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? - @key = key - @iv = iv - @use_padding = use_padding - end + key = compute_object_key(cipher) - def encrypt(data) + self.encoded_data = cipher.decrypt(key, @encoded_data) + @decrypted = true - if @iv.nil? - raise EncryptionError, "No initialization vector has been set." + self + end end - - if @use_padding - padlen = BLOCKSIZE - (data.size % BLOCKSIZE) - data << (padlen.chr * padlen) + + # + # Identity transformation. + # + module Identity + def Identity.encrypt(key, data) + data + end + + def Identity.decrypt(key, data) + data + end 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 + # + # Pure Ruby implementation of the RC4 symmetric algorithm + # + class RC4 - @iv + aes.update(data) + aes.final - else - cipher = [] - cipherblock = [] - nblocks = data.size / BLOCKSIZE + # + # Encrypts data using the given key + # + def RC4.encrypt(key, data) + RC4.new(key).encrypt(data) + end - first_round = true - nblocks.times do |n| - plainblock = data[n * BLOCKSIZE, BLOCKSIZE].unpack("C*") + # + # Decrypts data using the given key + # + def RC4.decrypt(key, data) + RC4.new(key).decrypt(data) + end - if first_round - BLOCKSIZE.times do |i| plainblock[i] ^= @iv[i].ord end - else - BLOCKSIZE.times do |i| plainblock[i] ^= cipherblock[i] end + # + # 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 end - first_round = false - cipherblock = aesEncrypt(plainblock) - cipher.concat(cipherblock) - end + # + # Encrypt/decrypt data with the RC4 encryption algorithm + # + def cipher(data) + return "" if data.empty? - @iv + cipher.pack("C*") - end - end + if Origami::OPTIONS[:use_openssl] + rc4 = OpenSSL::Cipher::RC4.new.encrypt + rc4.key_len = @key.length + rc4.key = @key - def decrypt(data) - unless data.size % BLOCKSIZE == 0 - raise EncryptionError, - "Data must be 16-bytes padded (data size = #{data.size} bytes)" + 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 + 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 - @iv = data.slice!(0, BLOCKSIZE) + # + # Pure Ruby implementation of the AES symmetric algorithm. + # Using mode CBC. + # + class AES + NROWS = 4 + NCOLS = 4 + BLOCKSIZE = NROWS * NCOLS - 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 + ROUNDS = + { + 16 => 10, + 24 => 12, + 32 => 14 + } - plain = (aes.update(data) + aes.final).unpack("C*") - else - plain = [] - plainblock = [] - prev_cipherblock = [] - nblocks = data.size / BLOCKSIZE + # + # 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 + ] - first_round = true - nblocks.times do |n| - cipherblock = data[n * BLOCKSIZE, BLOCKSIZE].unpack("C*") + # + # 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 + ] - plainblock = aesDecrypt(cipherblock) + 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 + ] - if first_round - BLOCKSIZE.times do |i| plainblock[i] ^= @iv[i].ord end - else - BLOCKSIZE.times do |i| plainblock[i] ^= prev_cipherblock[i] end + attr_writer :iv + + def AES.encrypt(key, iv, data) + AES.new(key, iv).encrypt(data) end - first_round = false - prev_cipherblock = cipherblock - plain.concat(plainblock) - end - end + def AES.decrypt(key, data) + AES.new(key, nil).decrypt(data) + end - if @use_padding - padlen = plain[-1] - unless (1..16) === padlen - raise EncryptionError, "Incorrect padding length : #{padlen}" - end + def initialize(key, iv, use_padding = true) + unless key.size == 16 or key.size == 24 or key.size == 32 + raise EncryptionError, "Key must have a length of 128, 192 or 256 bits." + end - padlen.times do - pad = plain.pop - raise EncryptionError, - "Incorrect padding byte : 0x#{pad.to_s 16}" if pad != padlen - end - end + if not iv.nil? and iv.size != BLOCKSIZE + raise EncryptionError, "Initialization vector must have a length of #{BLOCKSIZE} bytes." + end - plain.pack("C*") - end + @key = key + @iv = iv + @use_padding = use_padding + end - private + def encrypt(data) + if @iv.nil? + raise EncryptionError, "No initialization vector has been set." + end - def rol(row, n = 1) #:nodoc - n.times do row.push row.shift end ; row - end + if @use_padding + padlen = BLOCKSIZE - (data.size % BLOCKSIZE) + data << (padlen.chr * padlen) + end - def ror(row, n = 1) #:nodoc: - n.times do row.unshift row.pop end ; row - 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 - def galoisMult(a, b) #:nodoc: - p = 0 + @iv + aes.update(data) + aes.final + else + cipher = [] + cipherblock = [] + nblocks = data.size / BLOCKSIZE - 8.times do - p ^= a if b[0] == 1 - highBit = a[7] - a <<= 1 - a ^= 0x1b if highBit == 1 - b >>= 1 - end + first_round = true + nblocks.times do |n| + plainblock = data[n * BLOCKSIZE, BLOCKSIZE].unpack("C*") - p % 256 - end + if first_round + BLOCKSIZE.times do |i| plainblock[i] ^= @iv[i].ord end + else + BLOCKSIZE.times do |i| plainblock[i] ^= cipherblock[i] end + end - def scheduleCore(word, iter) #:nodoc: - rol(word) - word.map! do |byte| SBOX[byte] end - word[0] ^= RCON[iter] + first_round = false + cipherblock = aes_encrypt(plainblock) + cipher.concat(cipherblock) + end - word - end + @iv + cipher.pack("C*") + end + end - def transpose(m) #:nodoc: - [ - m[NROWS * 0, NROWS], - m[NROWS * 1, NROWS], - m[NROWS * 2, NROWS], - m[NROWS * 3, NROWS] - ].transpose.flatten - end + def decrypt(data) + unless data.size % BLOCKSIZE == 0 + raise EncryptionError, "Data must be 16-bytes padded (data size = #{data.size} bytes)" + end - # - # AES round methods. - # + @iv = data.slice!(0, BLOCKSIZE) - def createRoundKey(expandedKey, round = 0) #:nodoc: - transpose(expandedKey[round * BLOCKSIZE, BLOCKSIZE]) - end + 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 - def addRoundKey(roundKey) #:nodoc: - BLOCKSIZE.times do |i| @state[i] ^= roundKey[i] end - end + plain = (aes.update(data) + aes.final).unpack("C*") + else + plain = [] + plainblock = [] + prev_cipherblock = [] + nblocks = data.size / BLOCKSIZE - def subBytes #:nodoc: - BLOCKSIZE.times do |i| @state[i] = SBOX[ @state[i] ] end - end + first_round = true + nblocks.times do |n| + cipherblock = data[n * BLOCKSIZE, BLOCKSIZE].unpack("C*") - def rsubBytes #:nodoc: - BLOCKSIZE.times do |i| @state[i] = RSBOX[ @state[i] ] end - end + plainblock = aes_decrypt(cipherblock) - def shiftRows #:nodoc: - NROWS.times do |i| - @state[i * NCOLS, NCOLS] = rol(@state[i * NCOLS, NCOLS], i) - end - end + 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 - def rshiftRows #:nodoc: - NROWS.times do |i| - @state[i * NCOLS, NCOLS] = ror(@state[i * NCOLS, NCOLS], i) - end - end + first_round = false + prev_cipherblock = cipherblock + plain.concat(plainblock) + end + end - def mixColumnWithField(column, field) #:nodoc: - p = field - - column[0], column[1], column[2], column[3] = - galoisMult(column[0], p[0]) ^ galoisMult(column[3], p[1]) ^ galoisMult(column[2], p[2]) ^ galoisMult(column[1], p[3]), - galoisMult(column[1], p[0]) ^ galoisMult(column[0], p[1]) ^ galoisMult(column[3], p[2]) ^ galoisMult(column[2], p[3]), - galoisMult(column[2], p[0]) ^ galoisMult(column[1], p[1]) ^ galoisMult(column[0], p[2]) ^ galoisMult(column[3], p[3]), - galoisMult(column[3], p[0]) ^ galoisMult(column[2], p[1]) ^ galoisMult(column[1], p[2]) ^ galoisMult(column[0], p[3]) - end + if @use_padding + padlen = plain[-1] + unless (1..16) === padlen + raise EncryptionError, "Incorrect padding length : #{padlen}" + end - def mixColumn(column) #:nodoc: - mixColumnWithField(column, [ 2, 1, 1, 3 ]) - end + padlen.times do + pad = plain.pop + raise EncryptionError, "Incorrect padding byte : 0x#{pad.to_s 16}" if pad != padlen + end + end - def rmixColumn(column) #:nodoc: - mixColumnWithField(column, [ 14, 9, 13, 11 ]) - end + plain.pack("C*") + end - def mixColumns #:nodoc: - NCOLS.times do |c| - column = [] - NROWS.times do |r| column << @state[c + r * NCOLS] end - mixColumn(column) - NROWS.times do |r| @state[c + r * NCOLS] = column[r] end - end - end + private - def rmixColumns #:nodoc: - NCOLS.times do |c| - column = [] - NROWS.times do |r| column << @state[c + r * NCOLS] end - rmixColumn(column) - NROWS.times do |r| @state[c + r * NCOLS] = column[r] end - end - end + def rol(row, n = 1) #:nodoc + n.times do row.push row.shift end ; row + end - def expandKey(key) #:nodoc: + def ror(row, n = 1) #:nodoc: + n.times do row.unshift row.pop end ; row + end - key = key.unpack("C*") - size = key.size - expandedSize = 16 * (ROUNDS[key.size] + 1) - rconIter = 1 - expandedKey = key[0, size] + def galois_mult(a, b) #:nodoc: + p = 0 - while expandedKey.size < expandedSize - temp = expandedKey[-4, 4] + 8.times do + p ^= a if b[0] == 1 + highBit = a[7] + a <<= 1 + a ^= 0x1b if highBit == 1 + b >>= 1 + end - if expandedKey.size % size == 0 - scheduleCore(temp, rconIter) - rconIter = rconIter.succ - end + p % 256 + end - temp.map! do |b| SBOX[b] end if size == 32 and expandedKey.size % size == 16 + def schedule_core(word, iter) #:nodoc: + rol(word) + word.map! do |byte| SBOX[byte] end + word[0] ^= RCON[iter] - temp.each do |b| expandedKey << (expandedKey[-size] ^ b) end - end + word + end - expandedKey - end + def transpose(m) #:nodoc: + [ + m[NROWS * 0, NROWS], + m[NROWS * 1, NROWS], + m[NROWS * 2, NROWS], + m[NROWS * 3, NROWS] + ].transpose.flatten + end - def aesRound(roundKey) #:nodoc: - subBytes - #puts "after subBytes: #{@state.inspect}" - shiftRows - #puts "after shiftRows: #{@state.inspect}" - mixColumns - #puts "after mixColumns: #{@state.inspect}" - addRoundKey(roundKey) - #puts "roundKey = #{roundKey.inspect}" - #puts "after addRoundKey: #{@state.inspect}" - end + # + # AES round methods. + # - def raesRound(roundKey) #:nodoc: - addRoundKey(roundKey) - rmixColumns - rshiftRows - rsubBytes - end + def create_round_key(expanded_key, round = 0) #:nodoc: + transpose(expanded_key[round * BLOCKSIZE, BLOCKSIZE]) + end - def aesEncrypt(block) #:nodoc: - @state = transpose(block) - expandedKey = expandKey(@key) - rounds = ROUNDS[@key.size] + def add_round_key(roundKey) #:nodoc: + BLOCKSIZE.times do |i| @state[i] ^= roundKey[i] end + end - aesMain(expandedKey, rounds) - end + def sub_bytes #:nodoc: + BLOCKSIZE.times do |i| @state[i] = SBOX[ @state[i] ] end + end - def aesDecrypt(block) #:nodoc: - @state = transpose(block) - expandedKey = expandKey(@key) - rounds = ROUNDS[@key.size] + def r_sub_bytes #:nodoc: + BLOCKSIZE.times do |i| @state[i] = RSBOX[ @state[i] ] end + end - raesMain(expandedKey, rounds) - end + def shift_rows #:nodoc: + NROWS.times do |i| + @state[i * NCOLS, NCOLS] = rol(@state[i * NCOLS, NCOLS], i) + end + end - def aesMain(expandedKey, rounds) #:nodoc: - #puts "expandedKey: #{expandedKey.inspect}" - roundKey = createRoundKey(expandedKey) - addRoundKey(roundKey) + def r_shift_rows #:nodoc: + NROWS.times do |i| + @state[i * NCOLS, NCOLS] = ror(@state[i * NCOLS, NCOLS], i) + end + end - for i in 1..rounds-1 - roundKey = createRoundKey(expandedKey, i) - aesRound(roundKey) - end + def mix_column_with_field(column, field) #:nodoc: + p = field - roundKey = createRoundKey(expandedKey, rounds) - subBytes - shiftRows - addRoundKey(roundKey) + 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]), - transpose(@state) - end + galois_mult(column[1], p[0]) ^ + galois_mult(column[0], p[1]) ^ + galois_mult(column[3], p[2]) ^ + galois_mult(column[2], p[3]), - def raesMain(expandedKey, rounds) #:nodoc: - - roundKey = createRoundKey(expandedKey, rounds) - addRoundKey(roundKey) - rshiftRows - rsubBytes - - (rounds - 1).downto(1) do |i| - roundKey = createRoundKey(expandedKey, i) - raesRound(roundKey) - end + galois_mult(column[2], p[0]) ^ + galois_mult(column[1], p[1]) ^ + galois_mult(column[0], p[2]) ^ + galois_mult(column[3], p[3]), - roundKey = createRoundKey(expandedKey) - addRoundKey(roundKey) + 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 - transpose(@state) - end + def mix_column(column) #:nodoc: + mix_column_with_field(column, [ 2, 1, 1, 3 ]) + end - end - - # - # Class representing a crypt filter Dictionary - # - class CryptFilterDictionary < Dictionary - include StandardObject + def r_mix_column_(column) #:nodoc: + mix_column_with_field(column, [ 14, 9, 13, 11 ]) + end - field :Type, :Type => Name, :Default => :CryptFilter - field :CFM, :Type => Name, :Default => :None - field :AuthEvent, :Type => Name, :Default => :DocOpen - field :Length, :Type => Integer - 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 - # - # Common class for encryption dictionaries. - # - class EncryptionDictionary < Dictionary - include StandardObject + 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 - field :Filter, :Type => Name, :Default => :Standard, :Required => true - field :SubFilter, :Type => Name, :Version => "1.3" - field :V, :Type => Integer, :Default => 0 - field :Length, :Type => Integer, :Default => 40, :Version => "1.4" - field :CF, :Type => Dictionary, :Version => "1.5" - field :StmF, :Type => Name, :Default => :Identity, :Version => "1.5" - field :StrF, :Type => Name, :Default => :Identity, :Version => "1.5" - field :EFF, :Type => Name, :Version => "1.6" - end - - # - # The standard security handler for PDF encryption. - # - module Standard - - PADDING = "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A" #:nodoc: - PADDING.force_encoding('binary') if RUBY_VERSION >= '1.9' - - # - # Permission constants for encrypted documents. - # - module Permissions - RESERVED = 1 << 6 | 1 << 7 | 0xFFFFF000 - PRINT = 1 << 2 | RESERVED - MODIFY_CONTENTS = 1 << 3 | RESERVED - COPY_CONTENTS = 1 << 4 | RESERVED - MODIFY_ANNOTATIONS = 1 << 5 | RESERVED - FILLIN_FORMS = 1 << 8 | RESERVED - EXTRACT_CONTENTS = 1 << 9 | RESERVED - ASSEMBLE_DOC = 1 << 10 | RESERVED - HIGH_QUALITY_PRINT = 1 << 11 | RESERVED - - ALL = PRINT | MODIFY_CONTENTS | COPY_CONTENTS | MODIFY_ANNOTATIONS | FILLIN_FORMS | EXTRACT_CONTENTS | ASSEMBLE_DOC | HIGH_QUALITY_PRINT - end - - # - # Class defining a standard encryption dictionary. - # - class Dictionary < EncryptionDictionary - - field :R, :Type => Number, :Required => true - field :O, :Type => String, :Required => true - field :U, :Type => String, :Required => true - field :OE, :Type => String, :Version => '1.7', :ExtensionLevel => 3 - field :UE, :Type => String, :Version => '1.7', :ExtensionLevel => 3 - field :Perms, :Type => String, :Version => '1.7', :ExtensionLevel => 3 - field :P, :Type => Integer, :Default => 0, :Required => true - field :EncryptMetadata, :Type => Boolean, :Default => true, :Version => "1.5" + 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] - def pdf_version_required #:nodoc: - if self.R > 5 - [ 1.7, 8 ] - else - super - end - end - - # - # Computes the key that will be used to encrypt/decrypt the document contents with user password. - # - def compute_user_encryption_key(userpassword, fileid) - - if self.R < 5 - padded = pad_password(userpassword) - padded.force_encoding('binary') if RUBY_VERSION >= '1.9' + while expanded_key.size < expanded_size + temp = expanded_key[-4, 4] - padded << self.O - padded << [ self.P ].pack("i") - - padded << fileid - - encrypt_metadata = self.EncryptMetadata != false - padded << [ -1 ].pack("i") if self.R >= 4 and not encrypt_metadata + if expanded_key.size % size == 0 + schedule_core(temp, rcon_iter) + rcon_iter = rcon_iter.succ + end - key = Digest::MD5.digest(padded) + temp.map! do |b| SBOX[b] end if size == 32 and expanded_key.size % size == 16 - 50.times { key = Digest::MD5.digest(key[0, self.Length / 8]) } if self.R >= 3 + temp.each do |b| expanded_key << (expanded_key[-size] ^ b) end + end - if self.R == 2 - key[0, 5] - elsif self.R >= 3 - key[0, self.Length / 8] + expanded_key end - else - passwd = password_to_utf8(userpassword) - - uks = self.U[40, 8] - - if self.R == 5 - ukey = Digest::SHA256.digest(passwd + uks) - else - ukey = compute_hardened_hash(passwd, uks) + + 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 - - iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") - AES.new(ukey, nil, false).decrypt(iv + self.UE.value) - end - 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 r_aes_round(round_key) #:nodoc: + add_round_key(round_key) + r_mix_columns + r_shift_rows + r_sub_bytes + end - oks = self.O[40, 8] + def aes_encrypt(block) #:nodoc: + @state = transpose(block) + expanded_key = expand_key(@key) + rounds = ROUNDS[@key.size] - if self.R == 5 - okey = Digest::SHA256.digest(passwd + oks + self.U) - else - okey = compute_hardened_hash(passwd, oks, self.U) + aes_main(expanded_key, rounds) end - iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") - AES.new(okey, nil, false).decrypt(iv + self.OE.value) - end - end + def aes_decrypt(block) #:nodoc: + @state = transpose(block) + expanded_key = expand_key(@key) + rounds = ROUNDS[@key.size] - # - # Set up document passwords. - # - def set_passwords(ownerpassword, userpassword, salt = nil) - if self.R < 5 - key = compute_owner_key(ownerpassword) - upadded = pad_password(userpassword) - - owner_key = ARC4.encrypt(key, upadded) - 19.times { |i| owner_key = ARC4.encrypt(xor(key,i+1), owner_key) } if self.R >= 3 - - self.O = owner_key - self.U = compute_user_password(userpassword, salt) - - else - upass = password_to_utf8(userpassword) - opass = password_to_utf8(ownerpassword) + r_aes_main(expanded_key, rounds) + 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*") - - 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) + 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 - 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] + 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 - 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 + (rounds - 1).downto(1) do |i| + round_key = create_round_key(expanded_key, i) + r_aes_round(round_key) + end - self.Perms = AES.new(file_key, iv, false).encrypt(perms)[iv.size, 16] + round_key = create_round_key(expanded_key) + add_round_key(round_key) - file_key - end + transpose(@state) + end end - - # - # Checks user password. - # 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 - elsif self.R == 3 or self.R == 4 - compute_user_password(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] - compute_hardened_hash(password_to_utf8(pass), uvs) == self.U[0, 32] - end - end - - # - # Checks owner password. - # For version 2,3 and 4, _salt_ is the document ID. - # For version 5, _salt_ is (Owner Key Salt + U) - # - def is_owner_password?(pass, salt) - - if self.R < 5 - user_password = retrieve_user_password(pass) - is_user_password?(user_password, salt) - elsif self.R == 5 - ovs = self.O[32, 8] - Digest::SHA256.digest(password_to_utf8(pass) + ovs + self.U) == self.O[0, 32] - elsif self.R == 6 - ovs = self.O[32, 8] - compute_hardened_hash(password_to_utf8(pass), ovs, self.U[0,48]) == self.O[0, 32] - end - end # - # Retrieve user password from owner password. - # Cannot be used with revision 5. + # Class representing a crypt filter Dictionary # - def retrieve_user_password(ownerpassword) - key = compute_owner_key(ownerpassword) + class CryptFilterDictionary < Dictionary + include StandardObject - if self.R == 2 - ARC4.decrypt(key, self.O) - elsif self.R == 3 or self.R == 4 - user_password = ARC4.decrypt(xor(key, 19), self.O) - 19.times { |i| user_password = ARC4.decrypt(xor(key, 18-i), user_password) } - - user_password - end + field :Type, :Type => Name, :Default => :CryptFilter + field :CFM, :Type => Name, :Default => :None + field :AuthEvent, :Type => Name, :Default => :DocOpen + field :Length, :Type => Integer end - - private # - # Used to encrypt/decrypt the O field. - # Rev 2,3,4: O = crypt(user_pass, owner_key). - # Rev 5: unused. + # Common class for encryption dictionaries. # - def compute_owner_key(ownerpassword) #:nodoc: + class EncryptionDictionary < Dictionary + include StandardObject - opadded = pad_password(ownerpassword) - - hash = Digest::MD5.digest(opadded) - 50.times { hash = Digest::MD5.digest(hash) } if self.R >= 3 - - if self.R == 2 - hash[0, 5] - elsif self.R >= 3 - hash[0, self.Length / 8] - end + field :Filter, :Type => Name, :Default => :Standard, :Required => true + field :SubFilter, :Type => Name, :Version => "1.3" + field :V, :Type => Integer, :Default => 0 + field :Length, :Type => Integer, :Default => 40, :Version => "1.4" + field :CF, :Type => Dictionary, :Version => "1.5" + field :StmF, :Type => Name, :Default => :Identity, :Version => "1.5" + field :StrF, :Type => Name, :Default => :Identity, :Version => "1.5" + field :EFF, :Type => Name, :Version => "1.6" end - - # - # Compute the value of the U field. - # Cannot be used with revision 5. - # - def compute_user_password(userpassword, salt) #:nodoc: - - if self.R == 2 - key = compute_user_encryption_key(userpassword, salt) - user_key = ARC4.encrypt(key, PADDING) - elsif self.R == 3 or self.R == 4 - key = compute_user_encryption_key(userpassword, salt) - - upadded = PADDING + salt - hash = Digest::MD5.digest(upadded) - - user_key = ARC4.encrypt(key, hash) - - 19.times { |i| user_key = ARC4.encrypt(xor(key,i+1), user_key) } - - user_key.ljust(32, 0xFF.chr) - end - end # - # Computes hardened hash used in revision 6 (extension level 8). + # The standard security handler for PDF encryption. # - def compute_hardened_hash(password, salt, vector = '') - block_size = 32 - input = Digest::SHA256.digest(password + salt + vector) + "\x00" * 32 - key = input[0, 16] - iv = input[16, 16] - digest, aes, h, x = nil, nil, nil, nil + module Standard + PADDING = "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A".b #:nodoc: - i = 0 - while i < 64 or i < x[-1].ord + 32 - j = 0 - block = input[0, block_size] + # + # Permission constants for encrypted documents. + # + module Permissions + RESERVED = 1 << 6 | 1 << 7 | 0xFFFFF000 + PRINT = 1 << 2 | RESERVED + MODIFY_CONTENTS = 1 << 3 | RESERVED + COPY_CONTENTS = 1 << 4 | RESERVED + MODIFY_ANNOTATIONS = 1 << 5 | RESERVED + FILLIN_FORMS = 1 << 8 | RESERVED + EXTRACT_CONTENTS = 1 << 9 | RESERVED + ASSEMBLE_DOC = 1 << 10 | RESERVED + HIGH_QUALITY_PRINT = 1 << 11 | RESERVED - 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" + ALL = PRINT | MODIFY_CONTENTS | COPY_CONTENTS | + MODIFY_ANNOTATIONS | FILLIN_FORMS | EXTRACT_CONTENTS | + ASSEMBLE_DOC | HIGH_QUALITY_PRINT end - 64.times do |j| - x = '' - x += aes.update(password) unless password.empty? - x += aes.update(block) - x += aes.update(vector) unless vector.empty? + # + # Class defining a standard encryption dictionary. + # + class Dictionary < EncryptionDictionary - if j == 0 - block_size = 32 + (x.unpack("C16").inject(0) {|a,b| a+b} % 3) * 16 - digest = Digest::SHA2.new(block_size << 3) - end + field :R, :Type => Number, :Required => true + field :O, :Type => String, :Required => true + field :U, :Type => String, :Required => true + field :OE, :Type => String, :Version => '1.7', :ExtensionLevel => 3 + field :UE, :Type => String, :Version => '1.7', :ExtensionLevel => 3 + field :Perms, :Type => String, :Version => '1.7', :ExtensionLevel => 3 + field :P, :Type => Integer, :Default => 0, :Required => true + field :EncryptMetadata, :Type => Boolean, :Default => true, :Version => "1.5" - digest.update(x) - end + def version_required #:nodoc: + if self.R > 5 + [ 1.7, 8 ] + else + super + end + end - h = digest.digest - key = h[0, 16] - input[0, block_size] = h[0, block_size] - iv = h[16, 16] + # + # Computes the key that will be used to encrypt/decrypt the document contents with user password. + # + def compute_user_encryption_key(userpassword, fileid) + if self.R < 5 + padded = pad_password(userpassword) + padded.force_encoding('binary') - i = i + 1 - end + padded << self.O + padded << [ self.P ].pack("i") - h[0, 32] - end - - def xor(str, byte) #:nodoc: - str.split(//).map!{|c| (c[0].ord ^ byte).chr }.join - end - - def pad_password(password) #:nodoc: - return PADDING.dup if password.empty? # Fix for Ruby 1.9 bug - password[0,32].ljust(32, PADDING) - end + padded << fileid - def password_to_utf8(passwd) #:nodoc: - Origami::ByteString.new(passwd).to_utf8[0, 127] + encrypt_metadata = self.EncryptMetadata != false + padded << [ -1 ].pack("i") if self.R >= 4 and not encrypt_metadata + + key = Digest::MD5.digest(padded) + + 50.times { key = Digest::MD5.digest(key[0, self.Length / 8]) } if self.R >= 3 + + if self.R == 2 + key[0, 5] + elsif self.R >= 3 + key[0, self.Length / 8] + end + else + passwd = password_to_utf8(userpassword) + + uks = self.U[40, 8] + + if self.R == 5 + ukey = Digest::SHA256.digest(passwd + uks) + else + ukey = compute_hardened_hash(passwd, uks) + end + + iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") + AES.new(ukey, nil, false).decrypt(iv + self.UE.value) + end + 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) + + 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) + end + 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) + + owner_key = RC4.encrypt(key, upadded) + 19.times { |i| owner_key = RC4.encrypt(xor(key,i+1), owner_key) } if self.R >= 3 + + self.O = owner_key + self.U = compute_user_password(userpassword, salt) + + else + upass = password_to_utf8(userpassword) + opass = password_to_utf8(ownerpassword) + + 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 + 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 + + 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] + + 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.Perms = AES.new(file_key, iv, false).encrypt(perms)[iv.size, 16] + + file_key + end + end + + # + # Checks user password. + # 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 + elsif self.R == 3 or self.R == 4 + compute_user_password(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] + compute_hardened_hash(password_to_utf8(pass), uvs) == self.U[0, 32] + end + end + + # + # Checks owner password. + # For version 2,3 and 4, _salt_ is the document ID. + # For version 5, _salt_ is (Owner Key Salt + U) + # + def is_owner_password?(pass, salt) + + if self.R < 5 + user_password = retrieve_user_password(pass) + is_user_password?(user_password, salt) + elsif self.R == 5 + ovs = self.O[32, 8] + Digest::SHA256.digest(password_to_utf8(pass) + ovs + self.U) == self.O[0, 32] + elsif self.R == 6 + ovs = self.O[32, 8] + compute_hardened_hash(password_to_utf8(pass), ovs, self.U[0,48]) == self.O[0, 32] + end + end + + # + # Retrieve user password from owner password. + # Cannot be used with revision 5. + # + def retrieve_user_password(ownerpassword) + + key = compute_owner_key(ownerpassword) + + 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) + 19.times { |i| user_password = RC4.decrypt(xor(key, 18-i), user_password) } + + user_password + end + end + + private + + # + # 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: + + opadded = pad_password(ownerpassword) + + hash = Digest::MD5.digest(opadded) + 50.times { hash = Digest::MD5.digest(hash) } if self.R >= 3 + + if self.R == 2 + hash[0, 5] + elsif self.R >= 3 + hash[0, self.Length / 8] + end + end + + # + # Compute the value of the U field. + # Cannot be used with revision 5. + # + def compute_user_password(userpassword, salt) #:nodoc: + + if self.R == 2 + key = compute_user_encryption_key(userpassword, salt) + user_key = RC4.encrypt(key, PADDING) + elsif self.R == 3 or self.R == 4 + key = compute_user_encryption_key(userpassword, salt) + + upadded = PADDING + salt + hash = Digest::MD5.digest(upadded) + + user_key = RC4.encrypt(key, hash) + + 19.times { |i| user_key = RC4.encrypt(xor(key,i+1), user_key) } + + user_key.ljust(32, 0xFF.chr) + end + end + + # + # Computes hardened hash used in revision 6 (extension level 8). + # + def compute_hardened_hash(password, salt, vector = '') + block_size = 32 + input = Digest::SHA256.digest(password + salt + vector) + "\x00" * 32 + key = input[0, 16] + iv = input[16, 16] + digest, aes, h, x = nil, nil, nil, nil + + 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 + + 64.times do |j| + x = '' + x += aes.update(password) unless password.empty? + x += aes.update(block) + x += aes.update(vector) unless vector.empty? + + if j == 0 + block_size = 32 + (x.unpack("C16").inject(0) {|a,b| a+b} % 3) * 16 + digest = Digest::SHA2.new(block_size << 3) + end + + digest.update(x) + end + + h = digest.digest + key = h[0, 16] + input[0, block_size] = h[0, block_size] + iv = h[16, 16] + + i = i + 1 + end + + h[0, 32] + end + + def xor(str, byte) #:nodoc: + str.split(//).map!{|c| (c[0].ord ^ byte).chr }.join + end + + def pad_password(password) #:nodoc: + return PADDING.dup if password.empty? # Fix for Ruby 1.9 bug + password[0,32].ljust(32, PADDING) + end + + def password_to_utf8(passwd) #:nodoc: + LiteralString.new(passwd).to_utf8[0, 127] + end + end end - - end - end - - end end - -__END__ -def hexprint(str) - hex = "" - str.each_byte do |b| - digit = b.to_s(16) - digit = "0" + digit if digit.size == 1 - hex << digit - end - - puts hex.upcase -end -