lib/symmetric_encryption/cipher.rb in symmetric-encryption-2.2.0 vs lib/symmetric_encryption/cipher.rb in symmetric-encryption-3.0.0
- old
+ new
@@ -5,31 +5,42 @@
#
# 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_name, :version
- attr_accessor :encoding
+ attr_reader :cipher_name, :version, :iv
+ attr_accessor :encoding, :always_add_header
# Available encodings
ENCODINGS = [:none, :base64, :base64strict, :base16]
# Backward compatibility
alias_method :cipher, :cipher_name
+ # Defines the Header Structure returned when parsing the header
+ HeaderStruct = Struct.new(
+ :compressed, # [true|false] Whether the data is compressed, if supplied in the header
+ :binary, # [true|false] Whether the data is binary, if supplied in the header
+ :iv, # [String] IV used to encrypt the data, if supplied in the header
+ :key, # [String] Key used to encrypt the data, if supplied in the header
+ :cipher_name, # [String] Name of the cipher used, if supplied in the header
+ :version, # [Integer] Version of the cipher used, if supplied in the header
+ :decryption_cipher, # [SymmetricEncryption::Cipher] Cipher matching the header, or SymmetricEncryption.cipher(default_version)
+ )
+
# Generate a new Symmetric Key pair
#
# Returns a hash containing a new random symmetric_key pair
# consisting of a :key and :iv.
# 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)
+ def self.random_key_pair(cipher_name = 'aes-256-cbc')
openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
openssl_cipher.encrypt
{
:key => openssl_cipher.random_key,
- :iv => generate_iv ? openssl_cipher.random_iv : nil,
+ :iv => openssl_cipher.random_iv,
:cipher_name => cipher_name
}
end
# Create a Symmetric::Key for encryption and decryption purposes
@@ -63,22 +74,38 @@
#
# :version [Fixnum]
# Optional. The version number of this encryption key
# 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_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
+ #
+ # :always_add_header [true|false]
+ # Whether to always include the header when encrypting data.
+ # ** Highly recommended to set this value to true **
+ # Increases the length of the encrypted data by a few bytes, but makes
+ # migration to a new key trivial
+ # Default: false
+ # Recommended: true
+ #
+ def initialize(params={})
+ parms = params.dup
+ @key = parms.delete(:key)
+ @iv = parms.delete(:iv)
+ @cipher_name = parms.delete(:cipher_name) || parms.delete(:cipher) || 'aes-256-cbc'
+ @version = parms.delete(:version)
+ @always_add_header = parms.delete(:always_add_header) || false
+ @encoding = (parms.delete(:encoding) || :base64).to_sym
- raise("Invalid Encoding: #{@encoding}") unless ENCODINGS.include?(@encoding)
+ raise "Missing mandatory parameter :key" unless @key
+ raise "Invalid Encoding: #{@encoding}" unless ENCODINGS.include?(@encoding)
+ raise "Cipher version has a valid rage of 0 to 255. #{@version} is too high, or negative" if (@version.to_i > 255) || (@version.to_i < 0)
+ parms.each_pair {|k,v| warn "SymmetricEncryption::Cipher Ignoring unknown option #{k.inspect} = #{v.inspect}"}
end
- # Returns encrypted and then encoded string
+ # Encrypt and then encode a binary or UTF-8 string
+ #
+ # Returns data encrypted and then encoded according to the encoding setting
+ # of this cipher
# Returns nil if str is nil
# Returns "" str is empty
#
# Parameters
#
@@ -103,11 +130,11 @@
# 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
+ # the overhead of adding the encryption 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?
@@ -115,118 +142,132 @@
return str if str.empty?
encrypted = binary_encrypt(str, random_iv, compress)
self.encode(encrypted)
end
- # Decryption of supplied string
+ # Decode and Decrypt string
+ # Returns a decrypted string after decoding it first according to the
+ # encoding setting of this cipher
+ # Returns nil if encrypted_string is nil
+ # Returns '' if encrypted_string == ''
#
- # Decodes string first if decode is true
+ # Parameters
+ # encrypted_string [String]
+ # Binary encrypted string to decrypt
#
- # 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)
- decoded = self.decode(str)
- return unless decoded
+ # header [HeaderStruct]
+ # Optional header for the supplied encrypted_string
+ #
+ # binary [true|false]
+ # If no header is supplied then determines whether the string returned
+ # is binary or UTF8
+ #
+ # 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 and can be called concurrently by multiple threads with
+ # the same instance of Cipher
+ def decrypt(str)
+ decoded = self.decode(str)
+ return unless decoded
- return decoded if decoded.empty?
- binary_decrypt(decoded).force_encoding(SymmetricEncryption::UTF8_ENCODING)
- end
- else
- def decrypt(str)
- decoded = self.decode(str)
- return unless decoded
-
- return decoded if decoded.empty?
- crypt(:decrypt, decoded)
- end
+ return decoded if decoded.empty?
+ binary_decrypt(decoded).force_encoding(SymmetricEncryption::UTF8_ENCODING)
end
- # Return a new random key using the configured cipher_name
- # Useful for generating new symmetric keys
- def random_key
- ::OpenSSL::Cipher::Cipher.new(@cipher_name).random_key
- end
-
- # Returns the block size for the configured cipher_name
- def 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
# Returns nil if the supplied string is nil
# Note: No encryption or decryption is performed
#
# Returned string is UTF8 encoded except for encoding :none
def encode(binary_string)
- return unless binary_string
+ return binary_string if binary_string.nil? || (binary_string == '')
# Now encode data based on encoding setting
case encoding
when :base64
- ::Base64.encode64(binary_string).force_encoding(SymmetricEncryption::UTF8_ENCODING)
+ encoded_string = ::Base64.encode64(binary_string)
+ # Support Ruby 1.9 encoding
+ defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
when :base64strict
- ::Base64.encode64(binary_string).gsub(/\n/, '').force_encoding(SymmetricEncryption::UTF8_ENCODING)
+ encoded_string = ::Base64.encode64(binary_string).gsub(/\n/, '')
+ # Support Ruby 1.9 encoding
+ defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
when :base16
- binary_string.to_s.unpack('H*').first.force_encoding(SymmetricEncryption::UTF8_ENCODING)
+ encoded_string = binary_string.to_s.unpack('H*').first
+ # Support Ruby 1.9 encoding
+ defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
else
binary_string
end
end
# Decode the supplied string using the encoding in this cipher instance
# Note: No encryption or decryption is performed
#
# Returned string is Binary encoded
def decode(encoded_string)
- return unless encoded_string
+ return encoded_string if encoded_string.nil? || (encoded_string == '')
case encoding
when :base64, :base64strict
- ::Base64.decode64(encoded_string).force_encoding(SymmetricEncryption::BINARY_ENCODING)
+ decoded_string = ::Base64.decode64(encoded_string)
+ # Support Ruby 1.9 encoding
+ defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string
when :base16
- [encoded_string].pack('H*').force_encoding(SymmetricEncryption::BINARY_ENCODING)
+ decoded_string = [encoded_string].pack('H*')
+ # Support Ruby 1.9 encoding
+ defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string
else
encoded_string
end
end
- # 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]
+ # Return a new random key using the configured cipher_name
+ # Useful for generating new symmetric keys
+ def random_key
+ ::OpenSSL::Cipher::Cipher.new(@cipher_name).random_key
+ end
+
+ # Returns the block size for the configured cipher_name
+ def block_size
+ ::OpenSSL::Cipher::Cipher.new(@cipher_name).block_size
+ end
+
+ # Returns whether the supplied buffer starts with a symmetric_encryption header
+ # Note: The encoding of the supplied buffer is forced to binary if not already binary
+ def self.has_header?(buffer)
+ return false if buffer.nil? || (buffer == '')
+ buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING) if buffer.respond_to?(:force_encoding)
+ buffer.start_with?(MAGIC_HEADER)
+ end
+
+ # Returns HeaderStruct of the header parsed from the supplied string
+ # Returns nil if no header is present
#
- # The supplied buffer will be updated directly and will have the header
- # portion removed
+ # The supplied buffer will be updated directly and its header will be
+ # stripped if present
#
# Parameters
# buffer
- # String to extract the header from if present
+ # String to extract the header from
#
- # default_version
- # If no header is present, this is the default value for the version
- # of the cipher to use
- #
- # 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) if buffer
- return [default_compressed, nil, nil, nil, nil, SymmetricEncryption.cipher(default_version)] unless buffer && buffer.start_with?(MAGIC_HEADER)
+ def self.parse_header!(buffer)
+ return unless has_header?(buffer)
# Header includes magic header and version byte
# Remove header and extract flags
_, 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
+ binary = (flags & 0b0000_1000_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
@@ -236,18 +277,18 @@
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.binary_decrypt(buffer.slice!(0..len-1))
+ key = decryption_cipher.binary_decrypt(buffer.slice!(0..len-1), header=false, binary=true)
end
if include_cipher
len = buffer.slice!(0..1).unpack('v').first
cipher_name = buffer.slice!(0..len-1)
end
- [compressed, iv, key, cipher_name, version, decryption_cipher]
+ HeaderStruct.new(compressed, binary, iv, key, cipher_name, version, 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
#
@@ -267,34 +308,34 @@
#
# 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)
+ #
+ # binary
+ # Whether the data being encrypted is binary.
+ # When the header is read, it sets the encoding of the string returned to Binary
+ def self.build_header(version, compressed=false, iv=nil, key=nil, cipher_name=nil, binary=false)
# Ruby V2 named parameters would be perfect here
- # Encryption version indicator if available
- flags = version || 0 # Same as 0b0000_0000_0000_0000
+ # Version number of supplied encryption key, or use the global cipher version if none was supplied
+ flags = iv || key ? (SymmetricEncryption.cipher.version || 0) : (version || 0) # Same as 0b0000_0000_0000_0000
- # 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
flags |= 0b0010_0000_0000_0000 if key
flags |= 0b0001_0000_0000_0000 if cipher_name
+ flags |= 0b0000_1000_0000_0000 if binary
header = "#{MAGIC_HEADER}#{[flags].pack('v')}".force_encoding(SymmetricEncryption::BINARY_ENCODING)
if iv
header << [iv.length].pack('v')
header << iv
end
if key
- encrypted = SymmetricEncryption.cipher.binary_encrypt(key, false, false)
+ encrypted = SymmetricEncryption.cipher.binary_encrypt(key, false, false, false)
header << [encrypted.length].pack('v').force_encoding(SymmetricEncryption::BINARY_ENCODING)
header << encrypted
end
if cipher_name
header << [cipher_name.length].pack('v')
@@ -305,74 +346,119 @@
# 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
+ # add_header [nil|true|false]
+ # Whether to add a header to the encrypted string
+ # If not supplied it defaults to true if always_add_header || random_iv || compress
+ # Default: nil
#
# 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)
+ def binary_encrypt(str, random_iv=false, compress=false, add_header=nil)
+ return if str.nil?
+ string = str.to_s
+ return string if string.empty?
+
+ # Creates a new OpenSSL::Cipher with every call so that this call
+ # is thread-safe
openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name)
openssl_cipher.encrypt
openssl_cipher.key = @key
- result = if random_iv || compress
+ add_header = always_add_header || random_iv || compress if add_header.nil?
+ result = if add_header
# 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) +
+ # Set the binary indicator on the header if string is Binary Encoded
+ binary = (string.encoding == SymmetricEncryption::BINARY_ENCODING)
+ self.class.build_header(version, compress, random_iv ? iv : nil, binary) +
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
+ # See #decrypt to decrypt encoded strings
#
# Returns a Binary decrypted string without decoding the string first
#
+ # Decryption of supplied string
+ # Returns the decrypted string
+ # Returns nil if encrypted_string is nil
+ # Returns '' if encrypted_string == ''
+ #
+ # Parameters
+ # encrypted_string [String]
+ # Binary encrypted string to decrypt
+ #
+ # header [HeaderStruct]
+ # Optional header for the supplied encrypted_string
+ #
+ # binary [true|false]
+ # If no header is supplied then determines whether the string returned
+ # is binary or UTF8
+ #
# 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
+ # is thread-safe and can be called concurrently by multiple threads with
+ # the same instance of Cipher
#
- # See #decrypt to decrypt encoded strings
- def binary_decrypt(encrypted_string)
+ # Note:
+ # When a string is encrypted and the header is used, its decrypted form
+ # is automatically set to the same UTF-8 or Binary encoding
+ def binary_decrypt(encrypted_string, header=nil, binary=false)
+ return if encrypted_string.nil?
str = encrypted_string.to_s
- if str.start_with?(MAGIC_HEADER)
+ str.force_encoding(SymmetricEncryption::BINARY_ENCODING) if str.respond_to?(:force_encoding)
+ return str if str.empty?
+
+ decrypted_string = if header || self.class.has_header?(str)
str = str.dup
- compressed, iv, key, cipher_name = self.class.parse_magic_header!(str)
- openssl_cipher = ::OpenSSL::Cipher.new(cipher_name || self.cipher_name)
+ header ||= self.class.parse_header!(str)
+ binary = header.binary
+
+ openssl_cipher = ::OpenSSL::Cipher.new(header.cipher_name || self.cipher_name)
openssl_cipher.decrypt
- openssl_cipher.key = key || @key
- iv ||= @iv
+ openssl_cipher.key = header.key || @key
+ iv = header.iv || @iv
openssl_cipher.iv = iv if iv
result = openssl_cipher.update(str)
result << openssl_cipher.final
- compressed ? Zlib::Inflate.inflate(result) : result
+ header.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.update(str)
result << openssl_cipher.final
end
+
+ # Support Ruby 1.9 and above Encoding
+ if defined?(Encoding)
+ # Sets the encoding of the result string to UTF8 or BINARY based on the binary header
+ binary ? decrypted_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decrypted_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
+ else
+ decrypted_string
+ end
end
- # Returns [String] object represented as a string
- # Excluding the key and iv
+ # Returns [String] object represented as a string, filtering out the key
def inspect
- "#<#{self.class}:0x#{self.__id__.to_s(16)} @cipher_name=#{cipher_name.inspect}, @version=#{version.inspect}, @encoding=#{encoding.inspect}"
+ "#<#{self.class}:0x#{self.__id__.to_s(16)} @key=\"[FILTERED]\" @iv=#{iv.inspect} @cipher_name=#{cipher_name.inspect}, @version=#{version.inspect}, @encoding=#{encoding.inspect}"
end
private
- attr_reader :key, :iv
+ attr_reader :key
end
-end
\ No newline at end of file
+end