lib/symmetric_encryption/cipher.rb in symmetric-encryption-1.1.1 vs lib/symmetric_encryption/cipher.rb in symmetric-encryption-2.0.0

- old
+ new

@@ -5,54 +5,46 @@ # # Cipher is thread safe so that the same instance can be called by multiple # threads at the same time without needing an instance of Cipher per thread class Cipher # Cipher to use for encryption and decryption - attr_reader :cipher, :version + attr_reader :cipher_name, :version attr_accessor :encoding # Available encodings ENCODINGS = [:none, :base64, :base64strict, :base16] + # Backward compatibility + alias_method :cipher, :cipher_name + # Generate a new Symmetric Key pair # # Returns a hash containing a new random symmetric_key pair # consisting of a :key and :iv. - # The cipher is also included for compatibility with the Cipher initializer - def self.random_key_pair(cipher = 'aes-256-cbc', generate_iv = true) - openssl_cipher = OpenSSL::Cipher.new(cipher) + # The cipher_name is also included for compatibility with the Cipher initializer + def self.random_key_pair(cipher_name = 'aes-256-cbc', generate_iv = true) + openssl_cipher = ::OpenSSL::Cipher.new(cipher_name) openssl_cipher.encrypt { - :key => openssl_cipher.random_key, - :iv => generate_iv ? openssl_cipher.random_iv : nil, - :cipher => cipher + :key => openssl_cipher.random_key, + :iv => generate_iv ? openssl_cipher.random_iv : nil, + :cipher_name => cipher_name } end - # Returns a new Cipher with a random key and iv - # - # The cipher and encoding used are from the global encryption cipher - # - def self.random_cipher(cipher=nil, encoding=nil) - global_cipher = SymmetricEncryption.cipher - options = random_key_pair(cipher || global_cipher.cipher) - options[:encoding] = encoding || global_cipher.encoding - new(options) - end - # Create a Symmetric::Key for encryption and decryption purposes # # Parameters: # :key [String] # The Symmetric Key to use for encryption and decryption # # :iv [String] # Optional. The Initialization Vector to use with Symmetric Key # Highly Recommended as it is the input into the CBC algorithm # - # :cipher [String] + # :cipher_name [String] # Optional. Encryption Cipher to use # Default: aes-256-cbc # # :encoding [Symbol] # :base64strict @@ -74,85 +66,93 @@ # Used by SymmetricEncryption to select the correct key when decrypting data # Maximum value: 255 def initialize(parms={}) raise "Missing mandatory parameter :key" unless @key = parms[:key] @iv = parms[:iv] - @cipher = parms[:cipher] || 'aes-256-cbc' + @cipher_name = parms[:cipher_name] || parms[:cipher] || 'aes-256-cbc' @version = parms[:version] raise "Cipher version has a maximum of 255. #{@version} is too high" if @version.to_i > 255 @encoding = (parms[:encoding] || :base64).to_sym raise("Invalid Encoding: #{@encoding}") unless ENCODINGS.include?(@encoding) end - # Encryption of supplied string - # The String is encoded to UTF-8 prior to encryption + # Returns encrypted and then encoded string + # Returns nil if str is nil + # Returns "" str is empty # - # Returns result as an encoded string if encode is true - # Returns nil if the supplied str is nil - # Returns "" if it is a string and it is empty - if defined?(Encoding) - def encrypt(str, encode = true) - return if str.nil? - str = str.to_s #.force_encoding(SymmetricEncryption::BINARY_ENCODING) - return str if str.empty? - encrypted = crypt(:encrypt, str) - encode ? self.encode(encrypted) : encrypted - end - else - def encrypt(str, encode = true) - return if str.nil? - buf = str.to_s - return str if buf.empty? - encrypted = crypt(:encrypt, buf) - encode ? self.encode(encrypted) : encrypted - end + # Parameters + # + # str [String] + # String to be encrypted. If str is not a string, #to_s will be called on it + # to convert it to a string + # + # random_iv [true|false] + # Whether the encypted value should use a random IV every time the + # field is encrypted. + # It is recommended to set this to true where feasible. If the encrypted + # value could be used as part of a SQL where clause, or as part + # of any lookup, then it must be false. + # Setting random_iv to true will result in a different encrypted output for + # the same input string. + # Note: Only set to true if the field will never be used as part of + # the where clause in an SQL query. + # Note: When random_iv is true it will add a 8 byte header, plus the bytes + # to store the random IV in every returned encrypted string, prior to the + # encoding if any. + # Default: false + # Highly Recommended where feasible: true + # + # compress [true|false] + # Whether to compress str before encryption + # Should only be used for large strings since compression overhead and + # the overhead of adding the 'magic' header may exceed any benefits of + # compression + # Note: Adds a 6 byte header prior to encoding, only if :random_iv is false + # Default: false + def encrypt(str, random_iv=false, compress=false) + return if str.nil? + str = str.to_s + return str if str.empty? + encrypted = binary_encrypt(str, random_iv, compress) + self.encode(encrypted) end # Decryption of supplied string # # Decodes string first if decode is true # # Returns a UTF-8 encoded, decrypted string # Returns nil if the supplied str is nil # Returns "" if it is a string and it is empty if defined?(Encoding) - def decrypt(str, decode = true) - decoded = self.decode(str) if decode + def decrypt(str) + decoded = self.decode(str) return unless decoded return decoded if decoded.empty? - crypt(:decrypt, decoded).force_encoding(SymmetricEncryption::UTF8_ENCODING) + binary_decrypt(decoded).force_encoding(SymmetricEncryption::UTF8_ENCODING) end - - # Returns a binary decrypted string - def decrypt_binary(str, decode = true) - decoded = self.decode(str) if decode - return unless decoded - - return decoded if decoded.empty? - crypt(:decrypt, decoded).force_encoding(SymmetricEncryption::BINARY_ENCODING) - end else - def decrypt(str, decode = true) - decoded = self.decode(str) if decode + def decrypt(str) + decoded = self.decode(str) return unless decoded return decoded if decoded.empty? crypt(:decrypt, decoded) end end - # Return a new random key using the configured cipher + # Return a new random key using the configured cipher_name # Useful for generating new symmetric keys def random_key - ::OpenSSL::Cipher::Cipher.new(@cipher).random_key + ::OpenSSL::Cipher::Cipher.new(@cipher_name).random_key end - # Returns the block size for the configured cipher + # Returns the block size for the configured cipher_name def block_size - ::OpenSSL::Cipher::Cipher.new(@cipher).block_size + ::OpenSSL::Cipher::Cipher.new(@cipher_name).block_size end # Returns UTF8 encoded string after encoding the supplied Binary string # # Encode the supplied string using the encoding in this cipher instance @@ -191,17 +191,18 @@ else encoded_string end end - # Returns an Array with the first element being Symmetric Cipher that must - # be used to decrypt the data. The second element indicates whether the data - # must be decompressed after decryption + # Returns an Array of the following values extracted from header or nil + # if any value was not specified in the header + # compressed [true|false] + # iv [String] + # key [String] + # cipher_name [String} + # decryption_cipher [SymmetricEncryption::Cipher] # - # If the buffer does not start with the Magic Header the global cipher will - # be returned - # # The supplied buffer will be updated directly and will have the header # portion removed # # Parameters # buffer @@ -213,131 +214,165 @@ # # default_compressed # If no header is present, this is the default value for the compression def self.parse_magic_header!(buffer, default_version=nil, default_compressed=false) buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING) - return [SymmetricEncryption.cipher(default_version), default_compressed] unless buffer.start_with?(MAGIC_HEADER) + return [default_compressed, nil, nil, nil, SymmetricEncryption.cipher(default_version)] unless buffer.start_with?(MAGIC_HEADER) # Header includes magic header and version byte # Remove header and extract flags - header, flags = buffer.slice!(0..MAGIC_HEADER_SIZE+1).unpack(MAGIC_HEADER_UNPACK) + _, flags = buffer.slice!(0..MAGIC_HEADER_SIZE+1).unpack(MAGIC_HEADER_UNPACK) compressed = (flags & 0b1000_0000_0000_0000) != 0 include_iv = (flags & 0b0100_0000_0000_0000) != 0 include_key = (flags & 0b0010_0000_0000_0000) != 0 include_cipher= (flags & 0b0001_0000_0000_0000) != 0 + # Version of the key to use to decrypt the key if present, + # otherwise to decrypt the data following the header version = flags & 0b0000_0000_1111_1111 decryption_cipher = SymmetricEncryption.cipher(version) raise "Cipher with version:#{version.inspect} not found in any of the configured SymmetricEncryption ciphers" unless decryption_cipher - iv, key, cipher = nil + iv, key, cipher_name = nil if include_iv len = buffer.slice!(0..1).unpack('v').first iv = buffer.slice!(0..len-1) end if include_key len = buffer.slice!(0..1).unpack('v').first - key = decryption_cipher.send(:crypt, :decrypt, buffer.slice!(0..len-1)) + key = decryption_cipher.binary_decrypt(buffer.slice!(0..len-1)) end if include_cipher len = buffer.slice!(0..1).unpack('v').first - cipher = buffer.slice!(0..len-1) + cipher_name = buffer.slice!(0..len-1) end - if iv || key || cipher - decryption_cipher = SymmetricEncryption::Cipher.new( - :iv => iv, - :key => key || decryption_cipher.key, - :cipher => cipher || decryption_cipher.cipher - ) - end - - [decryption_cipher, compressed] + [compressed, iv, key, cipher_name, decryption_cipher] end # Returns a magic header for this cipher instance that can be placed at # the beginning of a file or stream to indicate how the data was encrypted # # Parameters # compressed # Sets the compressed indicator in the header + # Default: false # - # include_iv - # Includes the encrypted Initialization Vector from this cipher if present - # The IV is encrypted using the global encryption key + # iv + # The iv to to put in the header + # Default: nil : Exclude from header # - # include_key - # Includes the encrypted Key in this cipher + # key + # The key to to put in the header # The key is encrypted using the global encryption key + # Default: nil : Exclude key from header # - # include_cipher - # Includes the cipher used. For example 'aes-256-cbc' - # - # encryption_cipher - # Encryption cipher to use when encrypting the iv and key. - # When supplied, the version is set to it's version so that decryption - # knows which cipher to use - # Default: Global cipher: SymmetricEncryption.cipher - def magic_header(compressed=false, include_iv=false, include_key=false, include_cipher=false, encryption_cipher=nil) + # cipher_name + # Includes the cipher_name used. For example 'aes-256-cbc' + # The cipher_name string to to put in the header + # Default: nil : Exclude cipher_name name from header + def self.magic_header(version, compressed=false, iv=nil, key=nil, cipher_name=nil) # Ruby V2 named parameters would be perfect here # Encryption version indicator if available flags = version || 0 # Same as 0b0000_0000_0000_0000 - # Replace version with cipher used to encrypt Random IV and Key - if include_iv || include_key - encryption_cipher ||= SymmetricEncryption.cipher - flags = (encryption_cipher.version || 0) + # Replace version with global cipher that will be used to encrypt the random key + if iv || key + flags = (SymmetricEncryption.cipher.version || 0) end # If the data is to be compressed before being encrypted, set the # compressed bit in the flags word flags |= 0b1000_0000_0000_0000 if compressed - flags |= 0b0100_0000_0000_0000 if @iv && include_iv - flags |= 0b0010_0000_0000_0000 if include_key - flags |= 0b0001_0000_0000_0000 if include_cipher + flags |= 0b0100_0000_0000_0000 if iv + flags |= 0b0010_0000_0000_0000 if key + flags |= 0b0001_0000_0000_0000 if cipher_name header = "#{MAGIC_HEADER}#{[flags].pack('v')}".force_encoding(SymmetricEncryption::BINARY_ENCODING) - if @iv && include_iv - header << [@iv.length].pack('v') - header << @iv + if iv + header << [iv.length].pack('v') + header << iv end - if include_key - encrypted = encryption_cipher.crypt(:encrypt, @key).force_encoding(SymmetricEncryption::BINARY_ENCODING) + if key + encrypted = SymmetricEncryption.cipher.binary_encrypt(key, false, false) header << [encrypted.length].pack('v').force_encoding(SymmetricEncryption::BINARY_ENCODING) header << encrypted end - if include_cipher - header << [cipher.length].pack('v') - header << cipher + if cipher_name + header << [cipher_name.length].pack('v') + header << cipher_name end header end - protected - - # Only for use by Symmetric::EncryptedStream - def openssl_cipher(cipher_method) - openssl_cipher = ::OpenSSL::Cipher.new(self.cipher) - openssl_cipher.send(cipher_method) + # Advanced use only + # + # Returns a Binary encrypted string without applying any Base64, or other encoding + # + # Adds the 'magic' header if a random_iv is required or compression is enabled + # + # Creates a new OpenSSL::Cipher with every call so that this call + # is thread-safe + # + # See #encrypt to encrypt and encode the result as a string + def binary_encrypt(string, random_iv=false, compress=false) + openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name) + openssl_cipher.encrypt openssl_cipher.key = @key - openssl_cipher.iv = @iv if @iv - openssl_cipher + result = if random_iv || compress + # Random iv and compress both add the magic header + iv = random_iv ? openssl_cipher.random_iv : @iv + openssl_cipher.iv = iv if iv + self.class.magic_header(version, compress, random_iv ? iv : nil) + + openssl_cipher.update(compress ? Zlib::Deflate.deflate(string) : string) + else + openssl_cipher.iv = @iv if @iv + openssl_cipher.update(string) + end + result << openssl_cipher.final end + # Advanced use only + # + # Returns a Binary decrypted string without decoding the string first + # + # Reads the 'magic' header if present for key, iv, cipher_name and compression + # + # encrypted_string must be in raw binary form when calling this method + # # Creates a new OpenSSL::Cipher with every call so that this call # is thread-safe - # Return a binary encoded decrypted or encrypted string - def crypt(cipher_method, string) #:nodoc: - openssl_cipher = ::OpenSSL::Cipher.new(self.cipher) - openssl_cipher.send(cipher_method) - openssl_cipher.key = @key - openssl_cipher.iv = @iv if @iv - result = openssl_cipher.update(string) - result << openssl_cipher.final - result.force_encoding(SymmetricEncryption::BINARY_ENCODING) + # + # See #decrypt to decrypt encoded strings + def binary_decrypt(encrypted_string) + str = encrypted_string.to_s + if str.start_with?(MAGIC_HEADER) + str = str.dup + compressed, iv, key, cipher_name = self.class.parse_magic_header!(str) + openssl_cipher = ::OpenSSL::Cipher.new(cipher_name || self.cipher_name) + openssl_cipher.decrypt + openssl_cipher.key = key || @key + iv ||= @iv + openssl_cipher.iv = iv if iv + result = openssl_cipher.update(str) + result << openssl_cipher.final + compressed ? Zlib::Inflate.inflate(result) : result + else + openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name) + openssl_cipher.decrypt + openssl_cipher.key = @key + openssl_cipher.iv = @iv if @iv + result = openssl_cipher.update(encrypted_string) + result << openssl_cipher.final + end end + # Returns [String] object represented as a string + # Excluding the key and iv + def inspect + "#<#{self.class}:0x#{self.__id__.to_s(16)} @cipher_name=#{cipher_name.inspect}, @version=#{version.inspect}, @encoding=#{encoding.inspect}" + end + private attr_reader :key, :iv - end end \ No newline at end of file