Module: SymmetricEncryption

Defined in:
lib/symmetric_encryption/key.rb,
lib/symmetric_encryption/cli.rb,
lib/symmetric_encryption/core.rb,
lib/symmetric_encryption/cipher.rb,
lib/symmetric_encryption/coerce.rb,
lib/symmetric_encryption/config.rb,
lib/symmetric_encryption/header.rb,
lib/symmetric_encryption/reader.rb,
lib/symmetric_encryption/writer.rb,
lib/symmetric_encryption/encoder.rb,
lib/symmetric_encryption/railtie.rb,
lib/symmetric_encryption/rsa_key.rb,
lib/symmetric_encryption/version.rb,
lib/symmetric_encryption/keystore.rb,
lib/symmetric_encryption/exception.rb,
lib/symmetric_encryption/generator.rb,
lib/symmetric_encryption/utils/aws.rb,
lib/symmetric_encryption/utils/files.rb,
lib/symmetric_encryption/keystore/aws.rb,
lib/symmetric_encryption/keystore/gcp.rb,
lib/symmetric_encryption/keystore/file.rb,
lib/symmetric_encryption/keystore/heroku.rb,
lib/symmetric_encryption/keystore/memory.rb,
lib/symmetric_encryption/keystore/environment.rb,
lib/symmetric_encryption/symmetric_encryption.rb,
lib/symmetric_encryption/utils/re_encrypt_files.rb,
lib/symmetric_encryption/active_record/attr_encrypted.rb,
lib/symmetric_encryption/active_record/encrypted_attribute.rb

Overview

Used for re-encrypting encrypted passwords stored in configuration files.

Search for any encrypted value and re-encrypt it using the latest encryption key. Note:

  • Only works with encrypted values that have the standard header.

    • The search looks for the header and then replaces the encrypted value.

Example:

re_encrypt = SymmetricEncryption::Utils::ReEncryptFiles.new(version: 4)
re_encrypt.process_directory('../../**/*.yml')

Notes:

  • Only supports the output from encrypting data.

    • I.e. Manually adding newlines to base 64 output is not supported.

  • For now only supports one encrypted value per line.

Defined Under Namespace

Modules: ActiveRecord, Coerce, Encoder, Generator, Keystore, Utils Classes: CLI, Cipher, CipherError, Config, ConfigError, Error, Header, Key, RSAKey, Railtie, Reader, Writer

Constant Summary collapse

VERSION =
"4.6.0".freeze
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
%i[string integer float decimal datetime time date boolean json yaml].freeze
BINARY_ENCODING =
Encoding.find("binary")
UTF8_ENCODING =
Encoding.find("UTF-8")

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


46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 46

def self.cipher(version = nil)
  unless cipher?
    raise(
      SymmetricEncryption::ConfigError,
      "Call SymmetricEncryption.load! or SymmetricEncryption.cipher= prior to encrypting or decrypting data"
    )
  end

  return @cipher if version.nil? || (@cipher.version == version)

  secondary_ciphers.find { |c| c.version == version } || (@cipher if version.zero?)
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:    '1234567890ABCDEF',
  iv:     '1234567890ABCDEF',
  cipher: 'aes-128-cbc'
)


34
35
36
37
38
39
40
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 34

def self.cipher=(cipher)
  unless cipher.nil? || (cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt))
    raise(ArgumentError, "Cipher must respond to :encrypt and :decrypt")
  end

  @cipher = cipher
end

.cipher?Boolean

Returns whether a primary cipher has been set

Returns:

  • (Boolean)


60
61
62
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 60

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

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

Decrypt supplied string.

Returns [String] the decrypted string.
Returns [nil] if the supplied value is nil.
Returns [''] if it is a string and it is empty.

Parameters
  string [String]
    Encrypted string to decrypt.
  version [Integer]
    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


127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 127

def self.decrypt(encrypted_and_encoded_string, version: nil, type: :string)
  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?

  header    = Header.new
  decrypted =
    if header.parse!(decoded)
      header.cipher.binary_decrypt(decoded, header: header)
    else
      c =
        if version
          # Supplied version takes preference
          cipher(version)
        elsif @select_cipher
          # Use cipher_selector if present to decide which cipher to use
          @select_cipher.call(str, decoded)
        else
          # Global cipher
          cipher
        end
      c.binary_decrypt(decoded)
    end

  # 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
  Coerce.coerce_from_string(decrypted, type)
end

.encrypt(str, random_iv: SymmetricEncryption.randomize_iv?, compress: false, type: :string, header: cipher.always_add_header) ⇒ 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]
  Mandatory unless `SymmetricEncryption.randomize_iv = true` has been called.

  Whether the encrypted value should use a random IV every time the field is encrypted.
  It is recommended to set this to true where possible.

  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


220
221
222
223
224
225
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 220

def self.encrypt(str, random_iv: SymmetricEncryption.randomize_iv?, compress: false, type: :string, header: cipher.always_add_header)
  return str if str.nil? || (str == "")

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

.encrypted?(encrypted_data) ⇒ Boolean

Returns [true|false] whether the string is encrypted.

Notes:

  • This method only works reliably when the encrypted data includes the symmetric encryption header.

  • nil and ” are considered “encrypted” so that validations do not blow up on empty values.

Returns:

  • (Boolean)


249
250
251
252
253
254
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 249

def self.encrypted?(encrypted_data)
  return false if encrypted_data.nil? || (encrypted_data == "")

  @header ||= SymmetricEncryption.cipher.encoded_magic_header
  encrypted_data.to_s.start_with?(@header)
end

.header(encrypted_and_encoded_string) ⇒ Object

Returns the header for the encrypted string Returns [nil] if no header is present



165
166
167
168
169
170
171
172
173
174
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 165

def self.header(encrypted_and_encoded_string)
  return if encrypted_and_encoded_string.nil? || (encrypted_and_encoded_string == "")

  # Decode before decrypting supplied string
  decoded = cipher.encoder.decode(encrypted_and_encoded_string.to_s)
  return if decoded.nil? || decoded.empty?

  h = Header.new
  h.parse(decoded).zero? ? nil : h
end

.load!(file_name = nil, env = nil) ⇒ Object

Load the Encryption Configuration from a YAML file

file_name:
  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


293
294
295
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 293

def self.load!(file_name = nil, env = nil)
  Config.load!(file_name: file_name, env: env)
end

.random_password(size = 22) ⇒ Object

Generate a Random password



298
299
300
301
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 298

def self.random_password(size = 22)
  require "securerandom" unless defined?(SecureRandom)
  SecureRandom.urlsafe_base64(size)
end

.randomize_iv=(randomize_iv) ⇒ Object



88
89
90
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 88

def self.randomize_iv=(randomize_iv)
  @randomize_iv = randomize_iv
end

.randomize_iv?Boolean

Whether to randomize the iv by default. true: Generate a new random IV by default. [HIGHLY RECOMMENDED] false: Do not generate a new random IV by default.

Returns:

  • (Boolean)


84
85
86
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 84

def self.randomize_iv?
  @randomize_iv
end

.secondary_ciphersObject

Returns the Primary Symmetric Cipher being used



77
78
79
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 77

def self.secondary_ciphers
  @secondary_ciphers
end

.secondary_ciphers=(secondary_ciphers) ⇒ Object

Set the Secondary Symmetric Ciphers Array to be used

Raises:

  • (ArgumentError)


65
66
67
68
69
70
71
72
73
74
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 65

def self.secondary_ciphers=(secondary_ciphers)
  raise(ArgumentError, "secondary_ciphers must be a collection") unless secondary_ciphers.respond_to? :each

  secondary_ciphers.each do |cipher|
    unless cipher.respond_to?(:encrypt) && cipher.respond_to?(:decrypt)
      raise(ArgumentError, "secondary_ciphers can only consist of SymmetricEncryption::Ciphers")
    end
  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


281
282
283
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 281

def self.select_cipher(&block)
  @select_cipher = 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


238
239
240
241
242
# File 'lib/symmetric_encryption/symmetric_encryption.rb', line 238

def self.try_decrypt(str)
  decrypt(str)
rescue OpenSSL::Cipher::CipherError, SymmetricEncryption::CipherError
  nil
end