require "aws-sdk-kms" module SymmetricEncryption module Keystore # 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. class Aws include Utils::Files attr_reader :region, :key_files, :master_key_alias # 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"}, # {region: blah2, file_name: "~/symmetric-encryption/application_production_blah2_v6.encrypted_key"}, # ], # iv: 'T80pYzD0E6e/bJCdjZ6TiQ==' # } 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 # Alias pointing to the active version of the master key for that region. def self.master_key_alias(app_name, environment) @master_key_alias ||= "alias/symmetric-encryption/#{app_name}/#{environment}" end # Stores the Encryption key in a file. # Secures the Encryption key by encrypting it with a key encryption key. 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 # Reads the data key environment variable, if present, otherwise a file. # Decrypts the key using the master key for this region. 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 # Encrypt and write the data key to the file for each region. 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 def aws(region) Utils::Aws.new(region: region, master_key_alias: master_key_alias) end end end end