lib/symmetric/encryption.rb in symmetric-encryption-0.2.0 vs lib/symmetric/encryption.rb in symmetric-encryption-0.3.0
- old
+ new
@@ -8,240 +8,255 @@
# Encrypt using 256 Bit AES CBC symmetric key and initialization vector
# The symmetric key is protected using the private key below and must
# be distributed separately from the application
class Encryption
- # Binary encrypted data includes this magic header so that we can quickly
- # identify binary data versus base64 encoded data that does not have this header
- unless defined? MAGIC_HEADER
- MAGIC_HEADER = '@EnC'
- MAGIC_HEADER_SIZE = MAGIC_HEADER.size
+ # Defaults
+ @@cipher = nil
+ @@secondary_ciphers = []
+
+ # Set the Primary Symmetric Cipher to be used
+ def self.cipher=(cipher)
+ raise "Cipher must be similar to Symmetric::Ciphers" unless cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt) && cipher.respond_to?(:encrypted?)
+ @@cipher = cipher
end
- # The minimum length for an encrypted string
- def self.min_encrypted_length
- @@min_encrypted_length ||= encrypt('1').length
+ # Returns the Primary Symmetric Cipher being used
+ def self.cipher
+ @@cipher
end
- # Returns [true|false] a best effort determination as to whether the supplied
- # string is encrypted or not, without incurring the penalty of actually
- # decrypting the supplied data
- # Parameters:
- # encrypted_data: Encrypted string
- def self.encrypted?(encrypted_data)
- # Simple checks first
- return false if (encrypted_data.length < min_encrypted_length) || (!encrypted_data.end_with?("\n"))
- # For now have to decrypt it fully
- begin
- decrypt(encrypted_data) ? true : false
- rescue
- false
+ # Set the Secondary Symmetric Ciphers Array to be used
+ def self.secondary_ciphers=(secondary_ciphers)
+ raise "secondary_ciphers must be a collection" unless secondary_ciphers.respond_to? :each
+ secondary_ciphers.each do |cipher|
+ raise "secondary_ciphers can only consist of Symmetric::Ciphers" unless cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt) && cipher.respond_to?(:encrypted?)
end
+ @@secondary_ciphers = secondary_ciphers
end
- # Set the Symmetric Cipher to be used
- def self.cipher=(cipher)
- @@cipher = cipher
+ # Returns the Primary Symmetric Cipher being used
+ def self.secondary_ciphers
+ @@secondary_ciphers
end
- # Returns the Symmetric Cipher being used
- def self.cipher
- @@cipher
+ # AES Symmetric Decryption of supplied string
+ # Returns decrypted string
+ # Returns nil if the supplied str is nil
+ # Returns "" if it is a string and it is empty
+ #
+ # Note: If secondary ciphers are supplied in the configuration file the
+ # first key will be used to decrypt 'str'. If it fails each cipher in the
+ # order supplied will be tried.
+ # It is slow to try each cipher in turn, so should be used during migrations
+ # only
+ #
+ # Raises: OpenSSL::Cipher::CipherError when 'str' was not encrypted using
+ # the supplied key and iv
+ #
+ def self.decrypt(str)
+ raise "Call Symmetric::Encryption.load! or Symmetric::Encryption.cipher= prior to encrypting or decrypting data" unless @@cipher
+ begin
+ @@cipher.decrypt(str)
+ rescue OpenSSL::Cipher::CipherError => exc
+ @@secondary_ciphers.each do |cipher|
+ begin
+ return cipher.decrypt(str)
+ rescue OpenSSL::Cipher::CipherError
+ end
+ end
+ raise exc
+ end
end
- # Set the Symmetric Key to use for encryption and decryption
- def self.key=(key)
- @@key = key
+ # AES Symmetric Encryption of supplied string
+ # Returns result as a Base64 encoded string
+ # Returns nil if the supplied str is nil
+ # Returns "" if it is a string and it is empty
+ def self.encrypt(str)
+ raise "Call Symmetric::Encryption.load! or Symmetric::Encryption.cipher= prior to encrypting or decrypting data" unless @@cipher
+ @@cipher.encrypt(str)
end
- # Set the Initialization Vector to use with Symmetric Key
- def self.iv=(iv)
- @@iv = iv
+ # Invokes decrypt
+ # Returns decrypted String
+ # Return nil if it fails to decrypt a String
+ #
+ # Useful for example when decoding passwords encrypted using a key from a
+ # different environment. I.e. We cannot decode production passwords
+ # in the test or development environments but still need to be able to load
+ # YAML config files that contain encrypted development and production passwords
+ def self.try_decrypt(str)
+ raise "Call Symmetric::Encryption.load! or Symmetric::Encryption.cipher= prior to encrypting or decrypting data" unless @@cipher
+ begin
+ decrypt(str)
+ rescue OpenSSL::Cipher::CipherError
+ nil
+ end
end
- # Defaults
- @@key = nil
- @@iv = nil
+ # Returns [true|false] a best effort determination as to whether the supplied
+ # string is encrypted or not, without incurring the penalty of actually
+ # decrypting the supplied data
+ # Parameters:
+ # encrypted_data: Encrypted string
+ def self.encrypted?(encrypted_data)
+ raise "Call Symmetric::Encryption.load! or Symmetric::Encryption.cipher= prior to encrypting or decrypting data" unless @@cipher
+ @@cipher.encrypted?(encrypted_data)
+ end
# Load the Encryption Configuration from a YAML file
- # filename: Name of file to read.
+ # filename:
+ # Name of file to read.
# Mandatory for non-Rails apps
- # Default: Rails.root/config/symmetry.yml
+ # Default: Rails.root/config/symmetric-encryption.yml
+ # environment:
+ # Which environments config to load. Usually: production, development, etc.
+ # Default: Rails.env
def self.load!(filename=nil, environment=nil)
- config = YAML.load_file(filename || File.join(Rails.root, "config", "symmetric-encryption.yml"))[environment || Rails.env]
- self.cipher = config['cipher'] || 'aes-256-cbc'
- symmetric_key = config['symmetric_key']
- symmetric_iv = config['symmetric_iv']
+ config = read_config(filename, environment)
- # Hard coded symmetric_key?
- if symmetric_key
- self.key = symmetric_key
- self.iv = symmetric_iv
+ # Check for hard coded key, iv and cipher
+ if config[:key]
+ @@cipher = Cipher.new(config)
+ @@secondary_ciphers = []
else
- load_keys(config['symmetric_key_filename'], config['symmetric_iv_filename'], config['private_rsa_key'])
+ private_rsa_key = config[:private_rsa_key]
+ @@cipher, *@@secondary_ciphers = config[:ciphers].collect do |cipher_conf|
+ cipher_from_encrypted_files(
+ private_rsa_key,
+ cipher_conf[:cipher],
+ cipher_conf[:key_filename],
+ cipher_conf[:iv_filename])
+ end
end
+
true
end
- # Load the symmetric key to use for encrypting and decrypting data
- # Call from environment.rb before calling encrypt or decrypt
- #
- # private_key: Key used to unlock file containing the actual symmetric key
- def self.load_keys(key_filename, iv_filename, private_key)
- # Load Encrypted Symmetric keys
- encrypted_key = File.read(key_filename)
- encrypted_iv = File.read(iv_filename)
+ # Future: Generate private key in config file generator
+ #new_key = OpenSSL::PKey::RSA.generate(2048)
- # Decrypt Symmetric Key
- rsa = OpenSSL::PKey::RSA.new(private_key)
- @@key = rsa.private_decrypt(encrypted_key)
- @@iv = rsa.private_decrypt(encrypted_iv)
- nil
- end
-
# Generate new random symmetric keys for use with this Encryption library
#
+ # Note: Only the current Encryption key settings are used
+ #
# Creates Symmetric Key .key
# and initilization vector .iv
# which is encrypted with the above Public key
#
- # Note: Existing files will be overwritten
+ # Warning: Existing files will be overwritten
def self.generate_symmetric_key_files(filename=nil, environment=nil)
- # Temporary: Generate private key manually for now. Will automate soon.
- #new_key = OpenSSL::PKey::RSA.generate(2048)
+ config = read_config(filename, environment)
+ cipher_cfg = config[:ciphers].first
+ key_filename = cipher_cfg[:key_filename]
+ iv_filename = cipher_cfg[:iv_filename]
+ cipher = cipher_cfg[:cipher]
- filename ||= File.join(Rails.root, "config", "symmetric-encryption.yml")
- environment ||= (Rails.env || ENV['RAILS'])
- config = YAML.load_file(filename)[environment]
+ raise "The configuration file must contain a 'private_rsa_key' parameter to generate symmetric keys" unless config[:private_rsa_key]
+ rsa_key = OpenSSL::PKey::RSA.new(config[:private_rsa_key])
- raise "Missing mandatory 'key_filename' for environment:#{environment} in #{filename}" unless key_filename = config['symmetric_key_filename']
- iv_filename = config['symmetric_iv_filename']
- raise "Missing mandatory 'private_key' for environment:#{environment} in #{filename}" unless private_key = config['private_rsa_key']
- rsa_key = OpenSSL::PKey::RSA.new(private_key)
+ # Generate a new Symmetric Key pair
+ key_pair = Symmetric::Cipher.random_key_pair(cipher || 'aes-256-cbc', !iv_filename.nil?)
- # To ensure compatibility with C openssl code, remove RSA from pub file headers
- #File.open(File.join(rsa_keys_path, 'private.key'), 'w') {|file| file.write(new_key.to_pem)}
+ # Save symmetric key after encrypting it with the private RSA key, backing up existing files if present
+ File.rename(key_filename, "#{key_filename}.#{Time.now.to_i}") if File.exist?(key_filename)
+ File.open(key_filename, 'wb') {|file| file.write( rsa_key.public_encrypt(key_pair[:key]) ) }
- # Generate Symmetric Key
- openssl_cipher = OpenSSL::Cipher::Cipher.new(config['cipher'] || 'aes-256-cbc')
- openssl_cipher.encrypt
- @@key = openssl_cipher.random_key
- @@iv = openssl_cipher.random_iv if iv_filename
-
- # Save symmetric key after encrypting it with the private asymmetric key
- File.open(key_filename, 'wb') {|file| file.write( rsa_key.public_encrypt(@@key) ) }
- File.open(iv_filename, 'wb') {|file| file.write( rsa_key.public_encrypt(@@iv) ) } if iv_filename
+ if iv_filename
+ File.rename(iv_filename, "#{iv_filename}.#{Time.now.to_i}") if File.exist?(iv_filename)
+ File.open(iv_filename, 'wb') {|file| file.write( rsa_key.public_encrypt(key_pair[:iv]) ) }
+ end
puts("Generated new Symmetric Key for encryption. Please copy #{key_filename} and #{iv_filename} to the other web servers in #{environment}.")
end
# Generate a 22 character random password
def self.random_password
- Base64.encode64(OpenSSL::Cipher::Cipher.new('aes-128-cbc').random_key)[0..-4]
+ Base64.encode64(OpenSSL::Cipher.new('aes-128-cbc').random_key)[0..-4]
end
- # AES Symmetric Decryption of supplied string
- # Returns decrypted string
- # Returns nil if the supplied str is nil
- # Returns "" if it is a string and it is empty
- def self.decrypt(str)
- return str if str.nil? || (str.is_a?(String) && str.empty?)
- self.crypt(:decrypt, Base64.decode64(str))
- end
+ protected
- # AES Symmetric Encryption of supplied string
- # Returns result as a Base64 encoded string
- # Returns nil if the supplied str is nil
- # Returns "" if it is a string and it is empty
- def self.encrypt(str)
- return str if str.nil? || (str.is_a?(String) && str.empty?)
- Base64.encode64(self.crypt(:encrypt, str))
- end
-
- # Invokes decrypt
- # Returns decrypted String
- # Return nil if it fails to decrypt a String
+ # Returns the Encryption Configuration
#
- # Useful for example when decoding passwords encrypted using a key from a
- # different environment. I.e. We cannot decode production passwords
- # in the test or development environments but still need to be able to load
- # YAML config files that contain encrypted development and production passwords
- def self.try_decrypt(str)
- self.decrypt(str) rescue nil
- end
+ # Read the configuration from the YAML file and return in the latest format
+ #
+ # filename:
+ # Name of file to read.
+ # Mandatory for non-Rails apps
+ # Default: Rails.root/config/symmetric-encryption.yml
+ # environment:
+ # Which environments config to load. Usually: production, development, etc.
+ def self.read_config(filename=nil, environment=nil)
+ config = YAML.load_file(filename || File.join(Rails.root, "config", "symmetric-encryption.yml"))[environment || Rails.env]
- # AES Symmetric Encryption of supplied string
- # Returns result as a binary encrypted string
- # Returns nil if the supplied str is nil or empty
- # Parameters
- # compress => Whether to compress the supplied string using zip before
- # encrypting
- # true | false
- # Default false
- def self.encrypt_binary(str, compress=false)
- return nil if str.nil? || (str.is_a?(String) && str.empty?)
- # Bit Layout
- # 15 => Compressed?
- # 0..14 => Version number of encryption key/algorithm currently 0
- flags = 0 # Same as 0b0000_0000_0000_0000
- # If the data is to be compressed before being encrypted, set the flag and
- # compress using zlib. Only compress if data is greater than 15 chars
- str = str.to_s unless str.is_a?(String)
- if compress && str.length > 15
- flags |= 0b1000_0000_0000_0000
- begin
- ostream = StringIO.new
- gz = Zlib::GzipWriter.new(ostream)
- gz.write(str)
- str = ostream.string
- ensure
- gz.close
- end
- end
- return nil unless encrypted = self.crypt(:encrypt, str)
- # Resulting buffer consists of:
- # '@EnC'
- # unsigned short (32 bits) in little endian format for flags above
- # 'actual encrypted buffer data'
- "#{MAGIC_HEADER}#{[flags].pack('v')}#{encrypted}"
- end
+ # Default cipher
+ default_cipher = config['cipher'] || 'aes-256-cbc'
+ cfg = {}
- # AES Symmetric Decryption of supplied Binary string
- # Returns decrypted string
- # Returns nil if the supplied str is nil
- # Returns "" if it is a string and it is empty
- def self.decrypt_binary(str)
- return str if str.nil? || (str.is_a?(String) && str.empty?)
- str = str.to_s unless str.is_a?(String)
- encrypted = if str.starts_with? MAGIC_HEADER
- # Remove header and extract flags
- header, flags = str.unpack(@@unpack ||= "A#{MAGIC_HEADER_SIZE}v")
- # Uncompress if data is compressed and remove header
- if flags & 0b1000_0000_0000_0000
- begin
- gz = Zlib::GzipReader.new(StringIO.new(str[MAGIC_HEADER_SIZE,-1]))
- gz.read
- ensure
- gz.close
- end
- else
- str[MAGIC_HEADER_SIZE,-1]
+ # Hard coded symmetric_key? - Dev / Testing use only!
+ if symmetric_key = (config['key'] || config['symmetric_key'])
+ raise "Symmetric::Encryption Cannot hard code Production encryption keys in #{filename}" if (environment || Rails.env) == 'production'
+ cfg[:key] = symmetric_key
+ cfg[:iv] = config['iv'] || config['symmetric_iv']
+ cfg[:cipher] = default_cipher
+
+ elsif ciphers = config['ciphers']
+ raise "Missing mandatory config parameter 'private_rsa_key'" unless cfg[:private_rsa_key] = config['private_rsa_key']
+
+ cfg[:ciphers] = ciphers.collect do |cipher_cfg|
+ key_filename = cipher_cfg['key_filename'] || cipher_cfg['symmetric_key_filename']
+ raise "Missing mandatory 'key_filename' for environment:#{environment} in #{filename}" unless key_filename
+ iv_filename = cipher_cfg['iv_filename'] || cipher_cfg['symmetric_iv_filename']
+ {
+ :cipher => cipher_cfg['cipher'] || default_cipher,
+ :key_filename => key_filename,
+ :iv_filename => iv_filename,
+ }
end
+
else
- Base64.decode64(str)
+ # Migrate old format config
+ raise "Missing mandatory config parameter 'private_rsa_key'" unless cfg['private_rsa_key'] = config['private_rsa_key']
+ cfg[:ciphers] = [ {
+ :cipher => cipher['cipher'] || default_cipher,
+ :key_filename => config['symmetric_key_filename'],
+ :iv_filename => config['symmetric_iv_filename'],
+ } ]
end
- self.crypt(:decrypt, encrypted)
+
+ cfg
end
- protected
+ # Returns an instance of Symmetric::Cipher initialized from keys
+ # stored in files
+ #
+ # Raises an Exception on failure
+ #
+ # Parameters:
+ # cipher
+ # Encryption cipher for the symmetric encryption key
+ # private_key
+ # Key used to unlock file containing the actual symmetric key
+ # key_filename
+ # Name of file containing symmetric key encrypted using the public
+ # key matching the supplied private_key
+ # iv_filename
+ # Optional. Name of file containing symmetric key initialization vector
+ # encrypted using the public key matching the supplied private_key
+ def self.cipher_from_encrypted_files(private_rsa_key, cipher, key_filename, iv_filename = nil)
+ # Load Encrypted Symmetric keys
+ encrypted_key = File.read(key_filename)
+ encrypted_iv = File.read(iv_filename) if iv_filename
- def self.crypt(cipher_method, string) #:nodoc:
- openssl_cipher = OpenSSL::Cipher::Cipher.new(self.cipher)
- openssl_cipher.send(cipher_method)
- raise "Encryption.key must be set before calling Encryption encrypt or decrypt" unless @@key
- openssl_cipher.key = @@key
- openssl_cipher.iv = @@iv if @@iv
- result = openssl_cipher.update(string.to_s)
- result << openssl_cipher.final
+ # Decrypt Symmetric Keys
+ rsa = OpenSSL::PKey::RSA.new(private_rsa_key)
+ iv = rsa.private_decrypt(encrypted_iv) if iv_filename
+ Cipher.new(
+ :key => rsa.private_decrypt(encrypted_key),
+ :iv => iv,
+ :cipher => cipher
+ )
end
end
end
\ No newline at end of file