Module: SymmetricEncryption

Defined in:
lib/symmetric_encryption/railtie.rb,
lib/symmetric_encryption.rb,
lib/symmetric_encryption/cipher.rb,
lib/symmetric_encryption/reader.rb,
lib/symmetric_encryption/writer.rb,
lib/symmetric_encryption/version.rb,
lib/symmetric_encryption/generator.rb,
lib/symmetric_encryption/symmetric_encryption.rb,
lib/rails/generators/symmetric_encryption/config/config_generator.rb,
lib/rails/generators/symmetric_encryption/new_keys/new_keys_generator.rb,
lib/rails/generators/symmetric_encryption/heroku_config/heroku_config_generator.rb

Overview

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

Defined Under Namespace

Modules: Generator, Generators Classes: Cipher, Railtie, Reader, Writer

Constant Summary collapse

VERSION =
"3.6.0"
COERCION_TYPES =

List of types supported when encrypting or decrypting data

Each type maps to the built-in Ruby types as follows:

:string    => String
:integer   => Integer
:float     => Float
:decimal   => BigDecimal
:datetime  => DateTime
:time      => Time
:date      => Date
:json      => Uses JSON serialization, useful for hashes and arrays
:yaml      => Uses YAML serialization, useful for hashes and arrays
[:string, :integer, :float, :decimal, :datetime, :time, :date, :boolean, :json, :yaml]
MAGIC_HEADER =
'@EnC'
MAGIC_HEADER_SIZE =
MAGIC_HEADER.size
MAGIC_HEADER_UNPACK =
"a#{MAGIC_HEADER_SIZE}v"
@@cipher =

Defaults

nil
@@secondary_ciphers =
[]
@@select_cipher =
nil

Class Method Summary collapse

Class Method Details

.cipher(version = nil) ⇒ Object

Returns the Primary Symmetric Cipher being used If a version is supplied

Returns the primary cipher if no match was found and version == 0
Returns nil if no match was found and version != 0


49
50
51
52
53
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 49

def self.cipher(version = nil)
  raise "Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data" unless @@cipher
  return @@cipher if version.nil? || (@@cipher.version == version)
  secondary_ciphers.find {|c| c.version == version} || (@@cipher if version == 0)
end

.cipher=(cipher) ⇒ Object

Set the Primary Symmetric Cipher to be used

Example: For testing purposes the following test cipher can be used:

SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(
  key:    '1234567890ABCDEF1234567890ABCDEF',
  iv:     '1234567890ABCDEF',
  cipher: 'aes-128-cbc'
)


40
41
42
43
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 40

def self.cipher=(cipher)
  raise "Cipher must be similar to SymmetricEncryption::Ciphers" unless cipher.nil? || (cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt))
  @@cipher = cipher
end

.cipher?Boolean

Returns whether a primary cipher has been set

Returns:

  • (Boolean)


56
57
58
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 56

def self.cipher?
  !@@cipher.nil?
end

.decrypt(encrypted_and_encoded_string, version = nil, type = :string) ⇒ Object

AES Symmetric Decryption of supplied string

Returns decrypted value
Returns nil if the supplied value is nil
Returns "" if it is a string and it is empty

Parameters
  str
    Encrypted string to decrypt
  version
    Specify which cipher version to use if no header is present on the
    encrypted string
  type [:string|:integer|:float|:decimal|:datetime|:time|:date|:boolean]
    If value is set to something other than :string, then the coercible gem
    will be use to coerce the unencrypted string value into the specified
    type. This assumes that the value was stored using the same type.
    Note: If type is set to something other than :string, it's expected
      that the coercible gem is available in the path.
    Default: :string

If the supplied string has an encryption header then the cipher matching
the version number in the header will be used to decrypt the string

When no header is present in the encrypted data, a custom Block/Proc can
be supplied to determine which cipher to use to decrypt the data.
see #cipher_selector=

Raises: OpenSSL::Cipher::CipherError when ‘str’ was not encrypted using the primary key and iv

NOTE: #decrypt will not attempt to use a secondary cipher if it fails

to decrypt the current string. This is because in a very small
yet significant number of cases it is possible to decrypt data using
the incorrect key. Clearly the data returned is garbage, but it still
successfully returns a string of data


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 108

def self.decrypt(encrypted_and_encoded_string, version=nil, type=:string)
  raise "Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data" unless @@cipher
  return encrypted_and_encoded_string if encrypted_and_encoded_string.nil? || (encrypted_and_encoded_string == '')

  str = encrypted_and_encoded_string.to_s

  # Decode before decrypting supplied string
  decoded = @@cipher.decode(str)
  return unless decoded
  return decoded if decoded.empty?

  decrypted = if header = Cipher.parse_header!(decoded)
    header.decryption_cipher.binary_decrypt(decoded, header)
  else
    # Use cipher_selector if present to decide which cipher to use
    c = @@select_cipher.nil? ? cipher(version) : @@select_cipher.call(str, decoded)
    c.binary_decrypt(decoded)
  end

  if defined?(Encoding)
    # Try to force result to UTF-8 encoding, but if it is not valid, force it back to Binary
    unless decrypted.force_encoding(SymmetricEncryption::UTF8_ENCODING).valid_encoding?
      decrypted.force_encoding(SymmetricEncryption::BINARY_ENCODING)
    end
  end
  coerce_from_string(decrypted, type)
end

.encrypt(str, random_iv = false, compress = false, type = :string) ⇒ Object

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

Parameters

value [Object]
  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

type [:string|:integer|:float|:decimal|:datetime|:time|:date|:boolean]
  Expected data type of the value to encrypt
  Uses the coercible gem to coerce non-string values into string values.
  When type is set to :string (the default), uses #to_s to convert
  non-string values to string values.
  Note: If type is set to something other than :string, it's expected that
    the coercible gem is available in the path.
  Default: :string


178
179
180
181
182
183
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 178

def self.encrypt(str, random_iv=false, compress=false, type=:string)
  raise "Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data" unless @@cipher

  # Encrypt and then encode the supplied string
  @@cipher.encrypt(coerce_to_string(str, type), random_iv, compress)
end

.encrypted?(encrypted_data) ⇒ Boolean

Returns [true|false] as to whether the data could be decrypted

Parameters:
  encrypted_data: Encrypted string

WARNING: This method can only be relied upon if the encrypted data includes the

symmetric encryption header. In some cases data decrypted using the
wrong key will decrypt and return garbage

Returns:

  • (Boolean)


212
213
214
215
216
217
218
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 212

def self.encrypted?(encrypted_data)
  raise "Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data" unless @@cipher

  # For now have to decrypt it fully
  result = try_decrypt(encrypted_data)
  !(result.nil? || result == '')
end

.generate_symmetric_key_files(filename = nil, environment = nil) ⇒ Object

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

Existing key files will be renamed if present



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 273

def self.generate_symmetric_key_files(filename=nil, environment=nil)
  config_filename = filename || File.join(Rails.root, "config", "symmetric-encryption.yml")
  config = YAML.load(ERB.new(File.new(config_filename).read).result)[environment || Rails.env]

  # RSA key to decrypt key files
  private_rsa_key = config.delete('private_rsa_key')
  raise "The configuration file must contain a 'private_rsa_key' parameter to generate symmetric keys" unless private_rsa_key
  rsa_key = OpenSSL::PKey::RSA.new(private_rsa_key)

  # Check if config file contains 1 or multiple ciphers
  ciphers = config.delete('ciphers')
  cfg = ciphers.nil? ? config : ciphers.first

  # Convert keys to symbols
  cipher_cfg = {}
  cfg.each_pair{|k,v| cipher_cfg[k.to_sym] = v}

  cipher_name = cipher_cfg[:cipher_name] || cipher_cfg[:cipher]

  # Generate a new Symmetric Key pair
  iv_filename = cipher_cfg[:iv_filename]
  key_pair = SymmetricEncryption::Cipher.random_key_pair(cipher_name || 'aes-256-cbc')

  if key_filename = cipher_cfg[:key_filename]
    # 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]) ) }
    puts("Generated new Symmetric Key for encryption. Please copy #{key_filename} to the other web servers in #{environment}.")
  elsif !cipher_cfg[:key]
    key = rsa_key.public_encrypt(key_pair[:key])
    puts "Generated new Symmetric Key for encryption. Set the KEY environment variable in #{environment} to:"
    puts ::Base64.encode64(key)
  end

  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]) ) }
    puts("Generated new Symmetric Key for encryption. Please copy #{iv_filename} to the other web servers in #{environment}.")
  elsif !cipher_cfg[:iv]
    iv = rsa_key.public_encrypt(key_pair[:iv])
    puts "Generated new Symmetric Key for encryption. Set the IV environment variable in #{environment} to:"
    puts ::Base64.encode64(iv)
  end
end

.load!(filename = nil, environment = nil) ⇒ Object

Load the Encryption Configuration from a YAML file

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.
  Default: Rails.env


257
258
259
260
261
262
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 257

def self.load!(filename=nil, environment=nil)
  ciphers = read_config(filename, environment)
  @@cipher = ciphers.shift
  @@secondary_ciphers = ciphers
  true
end

.random_passwordObject

Generate a 22 character random password



319
320
321
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 319

def self.random_password
  Base64.encode64(OpenSSL::Cipher.new('aes-128-cbc').random_key)[0..-4].strip
end

.secondary_ciphersObject

Returns the Primary Symmetric Cipher being used



70
71
72
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 70

def self.secondary_ciphers
  @@secondary_ciphers
end

.secondary_ciphers=(secondary_ciphers) ⇒ Object

Set the Secondary Symmetric Ciphers Array to be used



61
62
63
64
65
66
67
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 61

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 SymmetricEncryption::Ciphers" unless cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt)
  end
  @@secondary_ciphers = secondary_ciphers
end

.select_cipher(&block) ⇒ Object

When no header is present in the encrypted data, this custom Block/Proc is used to determine which cipher to use to decrypt the data.

The Block must return a valid cipher

Parameters

encoded_str
  The original encoded string

decoded_str
  The string after being decoded using the global encoding

NOTE: Do not attempt to use a secondary cipher if the previous fails

to decrypt due to an OpenSSL::Cipher::CipherError exception.
This is because in a very small, yet significant number of cases it is
possible to decrypt data using the incorrect key.
Clearly the data returned is garbage, but it still successfully
returns a string of data

Example:

SymmetricEncryption.select_cipher do |encoded_str, decoded_str|
  # Use cipher version 0 if the encoded string ends with "\n" otherwise
  # use the current default cipher
  encoded_str.end_with?("\n") ? SymmetricEncryption.cipher(0) : SymmetricEncryption.cipher
end


245
246
247
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 245

def self.select_cipher(&block)
  @@select_cipher = block ? block : nil
end

.try_decrypt(str) ⇒ Object

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

WARNING: It is possible to decrypt data using the wrong key, so the value

returned should not be relied upon


196
197
198
199
200
201
202
203
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 196

def self.try_decrypt(str)
  raise "Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data" unless @@cipher
  begin
    decrypt(str)
  rescue OpenSSL::Cipher::CipherError
    nil
  end
end