lib/symmetric_encryption/cipher.rb in symmetric-encryption-3.7.2 vs lib/symmetric_encryption/cipher.rb in symmetric-encryption-3.8.0
- old
+ new
@@ -16,23 +16,33 @@
# 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
- :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)
+ # [true|false] Whether the data is compressed, if supplied in the header
+ :compressed,
+ # [String] IV used to encrypt the data, if supplied in the header
+ :iv,
+ # [String] Key used to encrypt the data, if supplied in the header
+ :key,
+ # [String] Name of the cipher used, if supplied in the header
+ :cipher_name,
+ # [Integer] Version of the cipher used, if supplied in the header
+ :version,
+ # [SymmetricEncryption::Cipher] Cipher matching the header, or SymmetricEncryption.cipher(default_version)
+ :decryption_cipher
)
# 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
+ #
+ # Notes:
+ # * The key _must_ be properly secured
+ # * The iv can be stored in the clear and it is not necessary to encrypt it
def self.random_key_pair(cipher_name = 'aes-256-cbc')
openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
openssl_cipher.encrypt
{
@@ -40,19 +50,81 @@
iv: openssl_cipher.random_iv,
cipher_name: cipher_name
}
end
+ # Generate new randomized keys and generate key and iv files if supplied
+ # Overwrites key files for the current environment
+ # See: #initialize for parameters
+ def self.generate_random_keys(params)
+ environment = params[:environment]
+ private_rsa_key = config[:private_rsa_key]
+ rsa = OpenSSL::PKey::RSA.new(private_rsa_key) if private_rsa_key
+ key_pair = SymmetricEncryption::Cipher.random_key_pair(params[:cipher_name] || 'aes-256-cbc')
+ key = key_pair[:key]
+ iv = key_pair[:iv]
+
+ puts 'Generated new Symmetric Key for encryption'
+ if params.has_key?(:key)
+ puts 'Put this value in your configuration file for :key'
+ p key
+ elsif file_name = config.delete(:key_filename)
+ write_to_file(file_name, key, rsa)
+ puts("Please copy #{file_name} to the other servers in #{environment}.")
+ elsif params.has_key?(:encrypted_key)
+ encrypted_key = encrypt_key(key, rsa)
+ puts 'If running in Heroku, add the environment specific key:'
+ puts "heroku config:add #{environment.upcase}_KEY1=#{encrypted_key}"
+ puts
+ puts 'Otherwise, set the :encrypted_key value to:'
+ puts encrypted_key
+ end
+
+ puts 'Generated new Initialization Vector for encryption'
+ if params.has_key?(:iv)
+ puts 'Put this value in your configuration file for :iv'
+ p iv
+ elsif file_name = config.delete(:iv_filename)
+ write_to_file(file_name, iv, rsa)
+ puts("Please copy #{file_name} to the other servers in #{environment}.")
+ elsif params.has_key?(:encrypted_iv)
+ encrypted_iv = encrypt_key(iv, rsa)
+ puts 'If running in Heroku, add the environment specific key:'
+ puts "heroku config:add #{environment.upcase}_KEY1=#{encrypted_iv}"
+ puts
+ puts 'Otherwise, set the :encrypted_iv value to:'
+ puts encrypted_iv
+ end
+ end
+
# Create a Symmetric::Key for encryption and decryption purposes
#
# Parameters:
# :key [String]
# The Symmetric Key to use for encryption and decryption
+ # Or,
+ # :key_filename
+ # Name of file containing symmetric key encrypted using the public
+ # key from the private_rsa_key
+ # Or,
+ # :encrypted_key
+ # Symmetric key encrypted using the public key from the private_rsa_key
+ # and then Base64 encoded
#
# :iv [String]
# Optional. The Initialization Vector to use with Symmetric Key
# Highly Recommended as it is the input into the CBC algorithm
+ # Or,
+ # Note: The following 2 options are deprecated since it is _not_ necessary
+ # to encrypt the initialization vector (IV)
+ # :iv_filename
+ # Name of file containing symmetric key initialization vector
+ # encrypted using the public key from the private_rsa_key
+ # Or,
+ # :encrypted_iv
+ # Initialization vector encrypted using the public key from the private_rsa_key
+ # and then Base64 encoded
#
# :cipher_name [String]
# Optional. Encryption Cipher to use
# Default: aes-256-cbc
#
@@ -82,20 +154,41 @@
# Increases the length of the encrypted data by a few bytes, but makes
# migration to a new key trivial
# Default: false
# Recommended: true
#
+ # private_rsa_key [String]
+ # RSA Key used to decrypt key and iv as applicable
+ # Mandatory if :key_filename, :encrypted_key, :iv_filename, or :encrypted_iv is supplied
def initialize(params={})
params = params.dup
- @key = params.delete(:key)
- @iv = params.delete(:iv)
@cipher_name = params.delete(:cipher_name) || params.delete(:cipher) || 'aes-256-cbc'
@version = params.delete(:version)
@always_add_header = params.delete(:always_add_header) || false
@encoding = (params.delete(:encoding) || :base64).to_sym
- raise(ArgumentError, "Missing mandatory parameter :key") unless @key
+ # To decrypt encrypted key or iv files
+ private_rsa_key = params.delete(:private_rsa_key)
+ rsa = OpenSSL::PKey::RSA.new(private_rsa_key) if private_rsa_key
+
+ if key = params.delete(:key)
+ @key = key
+ elsif file_name = params.delete(:key_filename)
+ @key = read_from_file(file_name, rsa)
+ elsif encrypted_key = params.delete(:encrypted_key)
+ @key = decrypt_key(encrypted_key, rsa)
+ end
+
+ if iv = params.delete(:iv)
+ @iv = iv
+ elsif file_name = params.delete(:iv_filename)
+ @iv = read_from_file(file_name, rsa)
+ elsif encrypted_iv = params.delete(:encrypted_iv)
+ @iv = decrypt_key(encrypted_iv, rsa)
+ end
+
+ raise(ArgumentError, 'Missing mandatory parameter :key, :key_filename, or :encrypted_key') unless @key
raise(ArgumentError, "Invalid Encoding: #{@encoding}") unless ENCODINGS.include?(@encoding)
raise(ArgumentError, "Cipher version has a valid range of 0 to 255. #{@version} is too high, or negative") if (@version.to_i > 255) || (@version.to_i < 0)
raise(ArgumentError, "SymmetricEncryption::Cipher Invalid options #{params.inspect}") if params.size > 0
end
@@ -187,20 +280,17 @@
# Now encode data based on encoding setting
case encoding
when :base64
encoded_string = ::Base64.encode64(binary_string)
- # Support Ruby 1.9 encoding
- defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
+ encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
when :base64strict
encoded_string = ::Base64.encode64(binary_string).gsub(/\n/, '')
- # Support Ruby 1.9 encoding
- defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
+ encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
when :base16
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
+ encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
else
binary_string
end
end
@@ -212,16 +302,14 @@
return encoded_string if encoded_string.nil? || (encoded_string == '')
case encoding
when :base64, :base64strict
decoded_string = ::Base64.decode64(encoded_string)
- # Support Ruby 1.9 encoding
- defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string
+ decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
when :base16
decoded_string = [encoded_string].pack('H*')
- # Support Ruby 1.9 encoding
- defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string
+ decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING)
else
encoded_string
end
end
@@ -275,32 +363,32 @@
# Key in binary form
# 2 Byte Cipher Name Length if included
# Cipher name it UTF8 text
# 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
+ _, 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
+ version = flags & 0b0000_0000_1111_1111
decryption_cipher = SymmetricEncryption.cipher(version)
raise(SymmetricEncryption::CipherError, "Cipher with version:#{version.inspect} not found in any of the configured SymmetricEncryption ciphers") unless decryption_cipher
- iv, key, cipher_name = 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.binary_decrypt(buffer.slice!(0..len-1), header=false)
end
if include_cipher
- len = buffer.slice!(0..1).unpack('v').first
+ len = buffer.slice!(0..1).unpack('v').first
cipher_name = buffer.slice!(0..len-1)
end
HeaderStruct.new(compressed, iv, key, cipher_name, version, decryption_cipher)
end
@@ -328,18 +416,18 @@
# Default: nil : Exclude cipher_name name from header
def self.build_header(version, compressed=false, iv=nil, key=nil, cipher_name=nil)
# Ruby V2 named parameters would be perfect here
# 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
+ flags = iv || key ? (SymmetricEncryption.cipher.version || 0) : (version || 0) # Same as 0b0000_0000_0000_0000
# 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 |= 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
header = "#{MAGIC_HEADER}#{[flags].pack('v')}".force_encoding(SymmetricEncryption::BINARY_ENCODING)
if iv
header << [iv.length].pack('v')
header << iv
end
@@ -376,22 +464,23 @@
# 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
- 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
- # Set the binary indicator on the header if string is Binary Encoded
- self.class.build_header(version, compress, random_iv ? iv : nil, nil, nil) +
- openssl_cipher.update(compress ? Zlib::Deflate.deflate(string) : string)
- else
- openssl_cipher.iv = @iv if @iv
- openssl_cipher.update(string)
- end
+ 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
+ # Set the binary indicator on the header if string is Binary Encoded
+ self.class.build_header(version, compress, random_iv ? iv : nil, nil, 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
# See #decrypt to decrypt encoded strings
@@ -427,27 +516,27 @@
str = encrypted_string.to_s
str.force_encoding(SymmetricEncryption::BINARY_ENCODING) if str.respond_to?(:force_encoding)
return str if str.empty?
if header || self.class.has_header?(str)
- str = str.dup
+ str = str.dup
header ||= self.class.parse_header!(str)
openssl_cipher = ::OpenSSL::Cipher.new(header.cipher_name || self.cipher_name)
openssl_cipher.decrypt
openssl_cipher.key = header.key || @key
- iv = header.iv || @iv
- openssl_cipher.iv = iv if iv
- result = openssl_cipher.update(str)
+ iv = header.iv || @iv
+ openssl_cipher.iv = iv if iv
+ result = openssl_cipher.update(str)
result << openssl_cipher.final
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(str)
+ openssl_cipher.iv = @iv if @iv
+ result = openssl_cipher.update(str)
result << openssl_cipher.final
end
end
# Returns [String] object represented as a string, filtering out the key
@@ -456,7 +545,48 @@
end
private
attr_reader :key
+
+ # Read the encrypted key from file
+ def read_from_file(file_name, rsa)
+ raise(SymmetricEncryption::ConfigError, 'Missing mandatory config parameter :private_rsa_key when filename key is used') unless rsa
+ begin
+ encrypted_key = File.open(file_name, 'rb') { |f| f.read }
+ rsa.private_decrypt(encrypted_key)
+ rescue Errno::ENOENT
+ puts "\nSymmetric Encryption key file: '#{file_name}' not found or readable."
+ puts "To generate the keys for the first time run: rails generate symmetric_encryption:new_keys\n\n"
+ end
+ end
+
+ # Save symmetric key after encrypting it with the private RSA key
+ # Backing up existing files if present
+ def write_to_file(file_name, key, rsa)
+ raise(SymmetricEncryption::ConfigError, 'Missing mandatory config parameter :private_rsa_key when filename key is used') unless rsa
+ File.rename(file_name, "#{file_name}.#{Time.now.to_i}") if File.exist?(file_name)
+ File.open(file_name, 'wb') { |file| file.write(rsa.public_encrypt(key)) }
+ end
+
+ # Read the encrypted key from file
+ def decrypt_key(encrypted_key, rsa)
+ raise(SymmetricEncryption::ConfigError, 'Missing mandatory config parameter :private_rsa_key when encrypted key is supplied') unless rsa
+
+ # Decode value first using encoding specified
+ encrypted_key = ::Base64.decode64(encrypted_key)
+ if !encrypted_key || encrypted_key.empty?
+ puts "\nSymmetric Encryption encrypted_key not found."
+ puts "To generate the keys for the first time run: rails generate symmetric_encryption:new_keys\n\n"
+ else
+ rsa.private_decrypt(encrypted_key)
+ end
+ end
+
+ # Returns [String] encrypted form of supplied key
+ def encrypt_key(key, rsa)
+ raise(SymmetricEncryption::ConfigError, 'Missing mandatory config parameter :private_rsa_key when encrypted key is supplied') unless rsa
+ ::Base64.encode64(rsa.public_encrypt(key))
+ end
+
end
end