lib/symmetric_encryption/cipher.rb in symmetric-encryption-1.0.0 vs lib/symmetric_encryption/cipher.rb in symmetric-encryption-1.1.1
- old
+ new
@@ -5,11 +5,11 @@
#
# 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, :version
+ attr_reader :cipher, :version
attr_accessor :encoding
# Available encodings
ENCODINGS = [:none, :base64, :base64strict, :base16]
@@ -27,10 +27,21 @@
:iv => generate_iv ? openssl_cipher.random_iv : nil,
:cipher => cipher
}
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
@@ -59,15 +70,17 @@
# Recommended: :base64strict
#
# :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 = 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
@@ -78,13 +91,13 @@
# 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?
- buf = str.to_s.encode(SymmetricEncryption::UTF8_ENCODING)
- return str if buf.empty?
- encrypted = crypt(:encrypt, buf)
+ 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?
@@ -105,22 +118,29 @@
if defined?(Encoding)
def decrypt(str, decode = true)
decoded = self.decode(str) if decode
return unless decoded
- buf = decoded.to_s.force_encoding(SymmetricEncryption::BINARY_ENCODING)
- return decoded if buf.empty?
- crypt(:decrypt, buf).force_encoding(SymmetricEncryption::UTF8_ENCODING)
+ return decoded if decoded.empty?
+ crypt(: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
return unless decoded
- buf = decoded.to_s
- return decoded if buf.empty?
- crypt(:decrypt, buf)
+ return decoded if decoded.empty?
+ crypt(:decrypt, decoded)
end
end
# Return a new random key using the configured cipher
# Useful for generating new symmetric keys
@@ -131,44 +151,168 @@
# Returns the block size for the configured cipher
def block_size
::OpenSSL::Cipher::Cipher.new(@cipher).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
# Now encode data based on encoding setting
case encoding
when :base64
- ::Base64.encode64(binary_string)
+ ::Base64.encode64(binary_string).force_encoding(SymmetricEncryption::UTF8_ENCODING)
when :base64strict
- ::Base64.encode64(binary_string).gsub(/\n/, '')
+ ::Base64.encode64(binary_string).gsub(/\n/, '').force_encoding(SymmetricEncryption::UTF8_ENCODING)
when :base16
- binary_string.to_s.unpack('H*').first
+ binary_string.to_s.unpack('H*').first.force_encoding(SymmetricEncryption::UTF8_ENCODING)
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
case encoding
when :base64, :base64strict
- ::Base64.decode64(encoded_string)
+ ::Base64.decode64(encoded_string).force_encoding(SymmetricEncryption::BINARY_ENCODING)
when :base16
- [encoded_string].pack('H*')
+ [encoded_string].pack('H*').force_encoding(SymmetricEncryption::BINARY_ENCODING)
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
+ #
+ # 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
+ # String to extract the header from if present
+ #
+ # 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)
+ return [SymmetricEncryption.cipher(default_version), default_compressed] 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)
+ 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 = 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
+
+ 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))
+ end
+ if include_cipher
+ len = buffer.slice!(0..1).unpack('v').first
+ cipher = 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]
+ 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
+ #
+ # include_iv
+ # Includes the encrypted Initialization Vector from this cipher if present
+ # The IV is encrypted using the global encryption key
+ #
+ # include_key
+ # Includes the encrypted Key in this cipher
+ # The key is encrypted using the global encryption key
+ #
+ # 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)
+ # 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)
+ 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
+ header = "#{MAGIC_HEADER}#{[flags].pack('v')}".force_encoding(SymmetricEncryption::BINARY_ENCODING)
+ if @iv && include_iv
+ header << [@iv.length].pack('v')
+ header << @iv
+ end
+ if include_key
+ encrypted = encryption_cipher.crypt(:encrypt, @key).force_encoding(SymmetricEncryption::BINARY_ENCODING)
+ header << [encrypted.length].pack('v').force_encoding(SymmetricEncryption::BINARY_ENCODING)
+ header << encrypted
+ end
+ if include_cipher
+ header << [cipher.length].pack('v')
+ header << cipher
+ end
+ header
+ end
+
protected
# Only for use by Symmetric::EncryptedStream
def openssl_cipher(cipher_method)
openssl_cipher = ::OpenSSL::Cipher.new(self.cipher)
@@ -186,10 +330,14 @@
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)
end
- end
+ private
+ attr_reader :key, :iv
+
+ end
end
\ No newline at end of file