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/symmetric_encryption.rb,
lib/rails/generators/symmetric_encryption/config/config_generator.rb,
lib/rails/generators/symmetric_encryption/new_keys/new_keys_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: Generators Classes: Cipher, Railtie, Reader, Writer

Constant Summary collapse

VERSION =
"2.2.0"
MAGIC_HEADER =
'@EnC'
MAGIC_HEADER_SIZE =
MAGIC_HEADER.size
MAGIC_HEADER_UNPACK =
"a#{MAGIC_HEADER_SIZE}v"
@@cipher =

Defaults

nil
@@secondary_ciphers =
[]

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

33
34
35
36
37
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 33

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'
)

24
25
26
27
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 24

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

.decrypt(str) ⇒ Object

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


67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 67

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

  # Decode and then decrypt supplied string
  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

.encrypt(str, random_iv = false, compress = false) ⇒ 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

str [String]
  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

117
118
119
120
121
122
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 117

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

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

.encrypted?(encrypted_data) ⇒ Boolean

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

Parameters:
  encrypted_data: Encrypted string

144
145
146
147
148
149
150
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 144

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


186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 186

def self.generate_symmetric_key_files(filename=nil, environment=nil)
  config = read_config(filename, environment)
  cipher_cfg = config[:ciphers].first
  key_filename = cipher_cfg[:key_filename]
  iv_filename = cipher_cfg[:iv_filename]
  cipher_name = cipher_cfg[:cipher_name] || cipher_cfg[:cipher]

  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])

  # Generate a new Symmetric Key pair
  key_pair = SymmetricEncryption::Cipher.random_key_pair(cipher_name || 'aes-256-cbc', !iv_filename.nil?)

  # 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]) ) }

  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

.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

160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 160

def self.load!(filename=nil, environment=nil)
  config = read_config(filename, environment)

  # Check for hard coded key, iv and cipher
  if config[:key]
    @@cipher = Cipher.new(config)
    @@secondary_ciphers = []
  else
    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)
    end
  end

  true
end

.random_passwordObject

Generate a 22 character random password


211
212
213
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 211

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

.secondary_ciphersObject

Returns the Primary Symmetric Cipher being used


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

def self.secondary_ciphers
  @@secondary_ciphers
end

.secondary_ciphers=(secondary_ciphers) ⇒ Object

Set the Secondary Symmetric Ciphers Array to be used


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

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

.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


132
133
134
135
136
137
138
139
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 132

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