Class: SymmetricEncryption::Keystore::Aws

Inherits:
Object
  • Object
show all
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(region: nil, key_files:, master_key_alias:, key_encrypting_key: nil) ⇒ Aws

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



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

def initialize(region: nil, key_files:, master_key_alias:, 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.



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

def key_files
  @key_files
end

#master_key_aliasObject (readonly)

Returns the value of attribute master_key_alias.



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

def master_key_alias
  @master_key_alias
end

#regionObject (readonly)

Returns the value of attribute region.



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

def region
  @region
end

Class Method Details

.generate_data_key(version: 0, regions: Utils::Aws::AWS_US_REGIONS, dek: nil, cipher_name:, app_name:, environment:, key_path:) ⇒ 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=='

}



72
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
107
108
# File 'lib/symmetric_encryption/keystore/aws.rb', line 72

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

  # 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.



111
112
113
# File 'lib/symmetric_encryption/keystore/aws.rb', line 111

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

Instance Method Details

#aws(region) ⇒ Object



157
158
159
# File 'lib/symmetric_encryption/keystore/aws.rb', line 157

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.



128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/symmetric_encryption/keystore/aws.rb', line 128

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]
  raise(SymmetricEncryption::ConfigError, 'file_name is mandatory for each key_file entry') unless file_name

  raise(SymmetricEncryption::ConfigError, "File #{file_name} could not be found") unless ::File.exist?(file_name)

  # TODO: Validate that file is not globally readable.
  encoded_dek        = ::File.open(file_name, 'rb', &:read)
  encrypted_data_key = Base64.strict_decode64(encoded_dek)
  aws(region).decrypt(encrypted_data_key)
end

#write(data_key) ⇒ Object

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



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

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)
    encoded_dek        = Base64.strict_encode64(encrypted_data_key)
    write_to_file(file_name, encoded_dek)
  end
end