Module: SymmetricEncryption::Keystore

Defined in:
lib/symmetric_encryption/keystore.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

Overview

Encryption keys are secured in Keystores

Defined Under Namespace

Classes: Aws, Environment, File, Gcp, Heroku, Memory

Class Method Summary collapse

Class Method Details

.camelize(term) ⇒ Object

Borrow from Rails, when not running Rails



202
203
204
205
206
207
208
# File 'lib/symmetric_encryption/keystore.rb', line 202

def self.camelize(term)
  string = term.to_s
  string = string.sub(/^[a-z\d]*/, &:capitalize)
  string.gsub!(%r{(?:_|(/))([a-z\d]*)}i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
  string.gsub!("/".freeze, "::".freeze)
  string
end

.constantize_symbol(symbol, namespace = "SymmetricEncryption::Keystore") ⇒ Object



192
193
194
195
196
197
198
199
# File 'lib/symmetric_encryption/keystore.rb', line 192

def self.constantize_symbol(symbol, namespace = "SymmetricEncryption::Keystore")
  klass = "#{namespace}::#{camelize(symbol.to_s)}"
  begin
    Object.const_get(klass)
  rescue NameError
    raise(ArgumentError, "Keystore: #{symbol.inspect} not found. Looking for: #{klass}")
  end
end

.dev_configObject

The default development config.



142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/symmetric_encryption/keystore.rb', line 142

def self.dev_config
  {
    ciphers:
             [
               {
                 key:         "1234567890ABCDEF",
                 iv:          "1234567890ABCDEF",
                 cipher_name: "aes-128-cbc",
                 version:     1
               }
             ]
  }
end

.generate_data_keys(keystore:, environments: %i[development test release production], **args) ⇒ Object

Returns [Hash] a new keystore configuration after generating data keys for each environment.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/symmetric_encryption/keystore.rb', line 14

def self.generate_data_keys(keystore:, environments: %i[development test release production], **args)
  keystore_class = keystore.is_a?(Symbol) || keystore.is_a?(String) ? constantize_symbol(keystore) : keystore

  configs = {}
  environments.each do |environment|
    environment          = environment.to_sym
    configs[environment] =
      if %i[development test].include?(environment)
        dev_config
      else
        cfg = keystore_class.generate_data_key(environment: environment, **args)
        {
          ciphers: [cfg]
        }
      end
  end
  configs
end

.keystore_for(config) ⇒ Object

Internal use only methods



178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/symmetric_encryption/keystore.rb', line 178

def self.keystore_for(config)
  if config[:keystore]
    constantize_symbol(config[:keystore])
  elsif config[:encrypted_key]
    Keystore::Memory
  elsif config[:key_filename]
    Keystore::File
  elsif config[:key_env_var]
    Keystore::Environment
  else
    raise(ArgumentError, "Unknown keystore supplied in config")
  end
end

.migrate_config!(config) ⇒ Object

Migrate a prior config.

Note:

  • The config cannot be saved back to the config file once migrated, without generating new Key Encrypting Keys.

  • Only run this migration in the target environment so that the current key encrypting files are present.



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/symmetric_encryption/keystore.rb', line 217

def self.migrate_config!(config)
  # Backward compatibility - Deprecated
  private_rsa_key = config.delete(:private_rsa_key)

  # Migrate old encrypted_iv
  if (encrypted_iv = config.delete(:encrypted_iv)) && private_rsa_key
    config[:iv] = RSAKey.new(private_rsa_key).decrypt(::Base64.decode64(encrypted_iv))
  end

  # Migrate old iv_filename
  if (file_name = config.delete(:iv_filename)) && private_rsa_key
    encrypted_iv = ::File.read(file_name)
    config[:iv]  = RSAKey.new(private_rsa_key).decrypt(encrypted_iv)
  end

  # Backward compatibility - Deprecated
  config[:key_encrypting_key] = RSAKey.new(private_rsa_key) if private_rsa_key

  # Migrate old encrypted_key to new binary format
  if (encrypted_key = config[:encrypted_key]) && private_rsa_key
    config[:encrypted_key] = ::Base64.decode64(encrypted_key)
  end
end

.read_key(iv:, key: nil, key_encrypting_key: nil, cipher_name: "aes-256-cbc", keystore: nil, version: 0, **args) ⇒ Object

Returns [Key] by recursively navigating the config tree.

Supports N level deep key encrypting keys.



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

def self.read_key(iv:, key: nil, key_encrypting_key: nil, cipher_name: "aes-256-cbc", keystore: nil, version: 0, **args)
  if key_encrypting_key.is_a?(Hash)
    # Recurse up the chain returning the parent key_encrypting_key
    key_encrypting_key = read_key(cipher_name: cipher_name, **key_encrypting_key)
  end

  unless key
    keystore_class = keystore ? constantize_symbol(keystore) : keystore_for(args)
    store          = keystore_class.new(key_encrypting_key: key_encrypting_key, **args)
    key            = store.read
  end

  Key.new(key: key, iv: iv, cipher_name: cipher_name)
end

.rotate_key_encrypting_keys!(full_config, app_name:, environments: []) ⇒ Object

Rotates just the key encrypting keys for the current cipher version. The existing data encryption key is not changed, it is secured using the new key encrypting keys.



98
99
100
101
102
103
104
105
106
107
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
135
136
137
138
139
# File 'lib/symmetric_encryption/keystore.rb', line 98

def self.rotate_key_encrypting_keys!(full_config, app_name:, environments: [])
  full_config.each_pair do |environment, cfg|
    # Only rotate keys for specified environments. Default, all
    next if !environments.empty? && !environments.include?(environment.to_sym)

    config = cfg[:ciphers].first

    # Only generate new keys for keystore's that have a key encrypting key
    next unless config[:key_encrypting_key]

    version = config.delete(:version) || 1
    version -= 1

    always_add_header = config.delete(:always_add_header)
    encoding          = config.delete(:encoding)

    migrate_config!(config)

    # The current data encrypting key without any of the key encrypting keys.
    key            = Keystore.read_key(config)
    cipher_name    = key.cipher_name
    keystore_class = keystore_for(config)

    args = {
      cipher_name: cipher_name,
      app_name:    app_name,
      version:     version,
      environment: environment,
      dek:         key
    }
    args[:key_path] = ::File.dirname(config[:key_filename]) if config.key?(:key_filename)

    new_config                     = keystore_class.generate_data_key(args)
    new_config[:always_add_header] = always_add_header
    new_config[:encoding]          = encoding

    # Replace existing config entry
    cfg[:ciphers].shift
    cfg[:ciphers].unshift(new_config)
  end
  full_config
end

.rotate_keys!(full_config, app_name:, environments: [], rolling_deploy: false, keystore: nil) ⇒ Object

Returns [Hash] a new configuration file after performing key rotation.

Perform key rotation for each of the environments in the configuration file, by

  • generating a new key, and iv with an incremented version number.

Params:

config: [Hash]
  The current contents of `symmetric-encryption.yml`.

environments: [Array<String>]
  List of environments for which to perform key rotation for.
  Default: All environments found in the current configuration file except development and test.

rolling_deploy: [true|false]
  To support a rolling deploy of the new key it must added initially as the second key.
  Then in a subsequent deploy the key can be moved into the first position to activate it.
  In this way during a rolling deploy encrypted values written by updated servers will be readable
  by the servers that have not been updated yet.
  Default: false

keystore: [Symbol]
  If supplied, changes the keystore during key rotation.

Notes:

  • iv_filename is no longer supported and is removed when creating a new random cipher.

    * `iv` does not need to be encrypted and is included in the clear.
    


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/symmetric_encryption/keystore.rb', line 59

def self.rotate_keys!(full_config, app_name:, environments: [], rolling_deploy: false, keystore: nil)
  full_config.each_pair do |environment, cfg|
    # Only rotate keys for specified environments. Default, all
    next if !environments.empty? && !environments.include?(environment.to_sym)

    # Find the highest version number
    version = cfg[:ciphers].collect { |c| c[:version] || 0 }.max

    config = cfg[:ciphers].first

    # Only generate new keys for keystore's that have a key encrypting key
    next unless config[:key_encrypting_key] || config[:private_rsa_key]

    cipher_name = config[:cipher_name] || "aes-256-cbc"

    keystore_class = keystore ? constantize_symbol(keystore) : keystore_for(config)

    args = {
      cipher_name: cipher_name,
      app_name:    app_name,
      version:     version,
      environment: environment
    }
    args[:key_path] = ::File.dirname(config[:key_filename]) if config.key?(:key_filename)
    new_data_key    = keystore_class.generate_data_key(**args)

    # Add as second key so that key can be published now and only used in a later deploy.
    if rolling_deploy
      cfg[:ciphers].insert(1, new_data_key)
    else
      cfg[:ciphers].unshift(new_data_key)
    end
  end
  full_config
end