Class: SymmetricEncryption::Keystore::Aws

Inherits:
Object
  • Object
show all
Includes:
Utils::Files
Defined in:
lib/symmetric_encryption/keystore/aws.rb

Overview

Support AWS Key Management Service (KMS)

Terms:

Aws
  Amazon Web Services.

CMK
  Customer Master Key.
  Master key to encrypt and decrypt data, specifically the DEK in this case.
  Stored in AWS, cannot be exported.

DEK
  Data Encryption Key.
  Key used to encrypt local data.
  Encrypted with the CMK and stored locally.

KMS
  Key Management Service.
  For generating and storing the CMK.
  Used to encrypt and decrypt the DEK.

Recommended reading:

Concepts:

https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html

Overview:

https://docs.aws.amazon.com/kms/latest/developerguide/overview.html

Process:

  1. Create a customer master key (CMK) along with an alias for use by Symmetric Encryption.

    - Note: CMK is region specific.
    - Stored exclusively in AWS KMS, cannot be exported.
    
  2. Generate and encrypt a data encryption key (DEK).

    - CMK is used to encrypt the DEK.
    - Encrypted DEK is stored locally.
    - Encrypted DEK is region specific.
      - DEK can be shared, but then must be re-encrypted in each region.
    - Shared DEK across regions for database replication.
    - List of regions to publish DEK to during generation / key-rotation.
    - DEK must be encrypted with CMK in each region consecutively.
    

Warning:

If access to the AWS KMS is ever lost, then it is not possible to decrypt any encrypted data.
Examples:
  - Loss of access to AWS accounts.
  - Loss of region(s) in which master keys are stored.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key_files:, master_key_alias:, region: nil, key_encrypting_key: nil) ⇒ Aws

Stores the Encryption key in a file. Secures the Encryption key by encrypting it with a key encryption key.



115
116
117
118
119
120
121
122
123
# File 'lib/symmetric_encryption/keystore/aws.rb', line 115

def initialize(key_files:, master_key_alias:, region: nil, key_encrypting_key: nil)
  @key_files        = key_files
  @master_key_alias = master_key_alias
  @region           = region || ENV["AWS_REGION"] || ENV["AWS_DEFAULT_REGION"] || ::Aws.config[:region]
  if key_encrypting_key
    raise(SymmetricEncryption::ConfigError,
          "AWS KMS keystore encrypts the key itself, so does not support supplying a key_encrypting_key")
  end
end

Instance Attribute Details

#key_filesObject (readonly)

Returns the value of attribute key_files.



55
56
57
# File 'lib/symmetric_encryption/keystore/aws.rb', line 55

def key_files
  @key_files
end

#master_key_aliasObject (readonly)

Returns the value of attribute master_key_alias.



55
56
57
# File 'lib/symmetric_encryption/keystore/aws.rb', line 55

def master_key_alias
  @master_key_alias
end

#regionObject (readonly)

Returns the value of attribute region.



55
56
57
# File 'lib/symmetric_encryption/keystore/aws.rb', line 55

def region
  @region
end

Class Method Details

.generate_data_key(cipher_name:, app_name:, environment:, key_path:, version: 0, regions: Utils::Aws::AWS_US_REGIONS, dek: nil, **_args) ⇒ Object

Returns [Hash] a new keystore configuration after generating the data key.

Increments the supplied version number by 1.

Sample Hash layout returned:

cipher_name: aes-256-cbc,
version:     8,
keystore:    :aws,
master_key_alias: 'alias/symmetric-encryption/application/production',
key_files:   [
               {region: blah1, file_name: "~/symmetric-encryption/application_production_blah1_v6.encrypted_key",
               blah2, file_name: "~/symmetric-encryption/application_production_blah2_v6.encrypted_key",
             ],
iv:          'T80pYzD0E6e/bJCdjZ6TiQ=='

}



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/symmetric_encryption/keystore/aws.rb', line 73

def self.generate_data_key(cipher_name:, app_name:, environment:, key_path:, version: 0,
                           regions: Utils::Aws::AWS_US_REGIONS,
                           dek: nil,
                           **_args)

  # TODO: Also support generating environment variables instead of files.

  version >= 255 ? (version = 1) : (version += 1)
  regions = Array(regions).dup

  master_key_alias = master_key_alias(app_name, environment)

  # File per region for holding the encrypted data key
  key_files = regions.collect do |region|
    file_name = "#{app_name}_#{environment}_#{region}_v#{version}.encrypted_key"
    {region: region, file_name: ::File.join(key_path, file_name)}
  end

  keystore = new(key_files: key_files, master_key_alias: master_key_alias)
  unless dek
    data_key = keystore.aws(regions.first).generate_data_key(cipher_name)
    dek      = Key.new(key: data_key, cipher_name: cipher_name)
  end
  keystore.write(dek.key)

  {
    keystore:         :aws,
    cipher_name:      dek.cipher_name,
    version:          version,
    master_key_alias: master_key_alias,
    key_files:        key_files,
    iv:               dek.iv
  }
end

.master_key_alias(app_name, environment) ⇒ Object

Alias pointing to the active version of the master key for that region.



109
110
111
# File 'lib/symmetric_encryption/keystore/aws.rb', line 109

def self.master_key_alias(app_name, environment)
  @master_key_alias ||= "alias/symmetric-encryption/#{app_name}/#{environment}"
end

Instance Method Details

#aws(region) ⇒ Object



150
151
152
# File 'lib/symmetric_encryption/keystore/aws.rb', line 150

def aws(region)
  Utils::Aws.new(region: region, master_key_alias: master_key_alias)
end

#readObject

Reads the data key environment variable, if present, otherwise a file. Decrypts the key using the master key for this region.



127
128
129
130
131
132
133
134
135
# File 'lib/symmetric_encryption/keystore/aws.rb', line 127

def read
  key_file = key_files.find { |i| i[:region] == region }
  raise(SymmetricEncryption::ConfigError, "region: #{region} not available in the supplied key_files") unless key_file

  file_name = key_file[:file_name]

  encrypted_data_key = read_file_and_decode(file_name)
  aws(region).decrypt(encrypted_data_key)
end

#write(data_key) ⇒ Object

Encrypt and write the data key to the file for each region.



138
139
140
141
142
143
144
145
146
147
148
# File 'lib/symmetric_encryption/keystore/aws.rb', line 138

def write(data_key)
  key_files.each do |key_file|
    region    = key_file[:region]
    file_name = key_file[:file_name]

    raise(ArgumentError, "region and file_name are mandatory for each key_file entry") unless region && file_name

    encrypted_data_key = aws(region).encrypt(data_key)
    write_encoded_to_file(file_name, encrypted_data_key)
  end
end