aws_advanced_ruby_driver_wrapper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +175 -0
- data/NOTICE +1 -0
- data/README.md +168 -0
- data/THIRD-PARTY-LICENSES +473 -0
- data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
- data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
- data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
- data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
- data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
- data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
- data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
- data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
- metadata +227 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require 'base64'
|
|
18
|
+
require 'digest'
|
|
19
|
+
require 'securerandom'
|
|
20
|
+
require_relative '../../logging'
|
|
21
|
+
require_relative '../../property_definition'
|
|
22
|
+
require_relative '../../utils/conversion_utils'
|
|
23
|
+
require_relative 'connection_source'
|
|
24
|
+
require_relative 'errors'
|
|
25
|
+
require_relative 'key_metadata'
|
|
26
|
+
|
|
27
|
+
module AwsAdvancedRubyDriverWrapper
|
|
28
|
+
module Plugins
|
|
29
|
+
module Encryption
|
|
30
|
+
# Owns the data keys: generates them through KMS, stores them in +key_storage+, and
|
|
31
|
+
# decrypts them again on the way back, caching the plaintext keys in memory.
|
|
32
|
+
#
|
|
33
|
+
# Plaintext data keys never reach the database. Only the KMS encrypted form is stored,
|
|
34
|
+
# and the plaintext copies handed out here are zeroed by their callers once used.
|
|
35
|
+
class KeyManager
|
|
36
|
+
include Logging
|
|
37
|
+
include Utils::ConversionUtils
|
|
38
|
+
include ConnectionSource
|
|
39
|
+
|
|
40
|
+
DATA_KEY_CACHE_PREFIX = 'datakey_'
|
|
41
|
+
HMAC_KEY_LENGTH = 32
|
|
42
|
+
KMS_KEY_SPEC = 'AES_256'
|
|
43
|
+
KMS_MASTER_KEY_SPEC = 'SYMMETRIC_DEFAULT'
|
|
44
|
+
KMS_KEY_USAGE = 'ENCRYPT_DECRYPT'
|
|
45
|
+
JITTER_RATIO = 0.25
|
|
46
|
+
MAX_BACKOFF_SEC = 20.0
|
|
47
|
+
|
|
48
|
+
# KMS error codes that are worth retrying.
|
|
49
|
+
RETRYABLE_ERROR_CODES = %w[
|
|
50
|
+
ThrottlingException
|
|
51
|
+
LimitExceededException
|
|
52
|
+
KMSInternalException
|
|
53
|
+
KeyUnavailableException
|
|
54
|
+
DependencyTimeoutException
|
|
55
|
+
RequestTimeout
|
|
56
|
+
ServiceUnavailable
|
|
57
|
+
InternalFailure
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
# A freshly generated data key, in both its plaintext and stored forms.
|
|
61
|
+
GeneratedDataKey = Data.define(:plaintext, :encrypted_data_key, :hmac_key)
|
|
62
|
+
|
|
63
|
+
# Exactly one of +connection+ or +service_container+ must be given (see {ConnectionSource}).
|
|
64
|
+
#
|
|
65
|
+
# @param kms_client [Aws::KMS::Client]
|
|
66
|
+
# @param sql_runner [SqlRunner]
|
|
67
|
+
# @param config [EncryptionConfig]
|
|
68
|
+
# @param data_key_cache [DataKeyCache]
|
|
69
|
+
# @param connection [Object, nil] a caller-owned connection used for every operation
|
|
70
|
+
# @param service_container [Services::ServiceContainer, nil] opens a short-lived connection per operation
|
|
71
|
+
# @param audit_logger [AuditLogger, nil]
|
|
72
|
+
def initialize(kms_client:, sql_runner:, config:, data_key_cache:, connection: nil, service_container: nil,
|
|
73
|
+
audit_logger: nil)
|
|
74
|
+
@kms_client = kms_client
|
|
75
|
+
use_connection_source(connection: connection, service_container: service_container)
|
|
76
|
+
@sql = sql_runner
|
|
77
|
+
@config = config
|
|
78
|
+
@cache = data_key_cache
|
|
79
|
+
@audit_logger = audit_logger
|
|
80
|
+
@schema = config.metadata_schema
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Decrypts a stored data key, using the in-memory cache when possible.
|
|
84
|
+
#
|
|
85
|
+
# The master key ARN comes from +key_storage+, so it is checked against the configured
|
|
86
|
+
# allow-list before anything else: a row repointed at another master key is refused rather
|
|
87
|
+
# than decrypted. KMS is then told which key to use, so a data key wrapped under a different
|
|
88
|
+
# key fails there too.
|
|
89
|
+
#
|
|
90
|
+
# @param encrypted_data_key [String] the base64 encoded, KMS encrypted data key
|
|
91
|
+
# @param master_key_arn [String, nil] the ARN of the master key the data key was wrapped with
|
|
92
|
+
# @return [String] the plaintext data key; the caller should wipe it once used
|
|
93
|
+
# @raise [Errors::KeyManagementError] if the master key is not allowed, or KMS rejects the request
|
|
94
|
+
def decrypt_data_key(encrypted_data_key, master_key_arn = nil)
|
|
95
|
+
authorize_master_key!(master_key_arn)
|
|
96
|
+
if encrypted_data_key.nil? || encrypted_data_key.to_s.strip.empty?
|
|
97
|
+
raise Errors::KeyManagementError
|
|
98
|
+
.invalid_key_metadata('The stored key metadata has no encrypted data key')
|
|
99
|
+
.with_master_key_arn(master_key_arn)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
cache_key = data_key_cache_key(encrypted_data_key)
|
|
103
|
+
cached = @cache.get(cache_key)
|
|
104
|
+
return cached if cached
|
|
105
|
+
|
|
106
|
+
plaintext = with_retry('DECRYPT_DATA_KEY') do
|
|
107
|
+
response = @kms_client.decrypt(
|
|
108
|
+
ciphertext_blob: Base64.strict_decode64(encrypted_data_key.to_s.delete("\n")),
|
|
109
|
+
**(master_key_arn ? { key_id: master_key_arn } : {})
|
|
110
|
+
)
|
|
111
|
+
response.plaintext.dup.b
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@cache.put(cache_key, plaintext)
|
|
115
|
+
@audit_logger&.log_data_key_decryption(master_key_arn: master_key_arn, success: true)
|
|
116
|
+
plaintext
|
|
117
|
+
rescue Errors::KeyManagementError => e
|
|
118
|
+
@audit_logger&.log_data_key_decryption(master_key_arn: master_key_arn, success: false, error_message: e.message)
|
|
119
|
+
raise
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Asks KMS for a new data key and generates the HMAC key that goes with it.
|
|
123
|
+
#
|
|
124
|
+
# @param master_key_arn [String] the ARN of the master key to wrap the data key with
|
|
125
|
+
# @return [GeneratedDataKey]
|
|
126
|
+
# @raise [Errors::KeyManagementError] if the master key is not allowed, or KMS rejects the request
|
|
127
|
+
def generate_data_key(master_key_arn)
|
|
128
|
+
authorize_master_key!(master_key_arn)
|
|
129
|
+
result = with_retry('GENERATE_DATA_KEY') do
|
|
130
|
+
response = @kms_client.generate_data_key(key_id: master_key_arn, key_spec: KMS_KEY_SPEC)
|
|
131
|
+
GeneratedDataKey.new(
|
|
132
|
+
plaintext: response.plaintext.dup.b,
|
|
133
|
+
encrypted_data_key: Base64.strict_encode64(response.ciphertext_blob),
|
|
134
|
+
hmac_key: SecureRandom.bytes(HMAC_KEY_LENGTH)
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
@audit_logger&.log_data_key_generation(master_key_arn: master_key_arn, success: true)
|
|
139
|
+
result
|
|
140
|
+
rescue Errors::KeyManagementError => e
|
|
141
|
+
@audit_logger&.log_data_key_generation(master_key_arn: master_key_arn, success: false, error_message: e.message)
|
|
142
|
+
raise
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Creates a new KMS master key.
|
|
146
|
+
#
|
|
147
|
+
# @param description [String] the key description
|
|
148
|
+
# @return [String] the ARN of the new key
|
|
149
|
+
# @raise [Errors::KeyManagementError] if KMS rejects the request
|
|
150
|
+
def create_master_key(description)
|
|
151
|
+
arn = with_retry('CREATE_MASTER_KEY') do
|
|
152
|
+
response = @kms_client.create_key(
|
|
153
|
+
description: description,
|
|
154
|
+
key_usage: KMS_KEY_USAGE,
|
|
155
|
+
key_spec: KMS_MASTER_KEY_SPEC
|
|
156
|
+
)
|
|
157
|
+
response.key_metadata.arn
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
@audit_logger&.log_key_creation(master_key_arn: arn, description: description, success: true)
|
|
161
|
+
arn
|
|
162
|
+
rescue Errors::KeyManagementError => e
|
|
163
|
+
@audit_logger&.log_key_creation(master_key_arn: nil, description: description, success: false, error_message: e.message)
|
|
164
|
+
raise
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Checks that a master key exists, is enabled, and can encrypt and decrypt.
|
|
168
|
+
#
|
|
169
|
+
# @param master_key_arn [String]
|
|
170
|
+
# @return [Boolean]
|
|
171
|
+
def validate_master_key(master_key_arn)
|
|
172
|
+
metadata = with_retry('DESCRIBE_MASTER_KEY') { @kms_client.describe_key(key_id: master_key_arn).key_metadata }
|
|
173
|
+
metadata.enabled && metadata.key_state == 'Enabled' && metadata.key_usage == KMS_KEY_USAGE
|
|
174
|
+
rescue Errors::KeyManagementError => e
|
|
175
|
+
logger.warn("Master key validation failed: #{e.message}")
|
|
176
|
+
false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Inserts a row into +key_storage+.
|
|
180
|
+
#
|
|
181
|
+
# @param key_metadata [KeyMetadata] the key to store; its +id+ is ignored
|
|
182
|
+
# @return [KeyMetadata] the stored key, with +id+ and timestamps filled in
|
|
183
|
+
# @raise [Errors::KeyManagementError] if the row cannot be written
|
|
184
|
+
def store_key_metadata(key_metadata)
|
|
185
|
+
unless key_metadata.valid?
|
|
186
|
+
raise Errors::KeyManagementError.invalid_key_metadata(
|
|
187
|
+
'Refusing to store key metadata that names no master key or carries no encrypted data key'
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
now = Time.now
|
|
192
|
+
to_store = key_metadata.with(
|
|
193
|
+
key_id: key_metadata.key_id || generate_key_id,
|
|
194
|
+
created_at: key_metadata.created_at || now,
|
|
195
|
+
last_used_at: key_metadata.last_used_at || now
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
id = with_connection(operation: 'STORE_KEY_METADATA') do |connection|
|
|
199
|
+
@sql.insert_returning_id(connection, insert_key_sql, [
|
|
200
|
+
to_store.key_id,
|
|
201
|
+
to_store.key_name,
|
|
202
|
+
to_store.master_key_arn,
|
|
203
|
+
to_store.encrypted_data_key,
|
|
204
|
+
@sql.binary_param(to_store.hmac_key),
|
|
205
|
+
to_store.key_spec,
|
|
206
|
+
to_store.created_at,
|
|
207
|
+
to_store.last_used_at
|
|
208
|
+
])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
to_store.with(id: id)
|
|
212
|
+
rescue StandardError => e
|
|
213
|
+
raise wrap_storage_error(e, to_store&.key_id)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Reads one row of +key_storage+ by its surrogate id.
|
|
217
|
+
#
|
|
218
|
+
# @param id [Integer] the +key_storage.id+ value
|
|
219
|
+
# @return [KeyMetadata, nil]
|
|
220
|
+
# @raise [Errors::KeyManagementError] if the row cannot be read
|
|
221
|
+
def key_metadata_by_id(id)
|
|
222
|
+
row = with_connection(operation: 'GET_KEY_METADATA') do |connection|
|
|
223
|
+
@sql.query(connection, select_key_sql, [id]).first
|
|
224
|
+
end
|
|
225
|
+
row && to_key_metadata(row)
|
|
226
|
+
rescue StandardError => e
|
|
227
|
+
raise Errors::KeyManagementError
|
|
228
|
+
.key_retrieval_failed("Failed to read key metadata: #{e.message}")
|
|
229
|
+
.with_key_id(id.to_s)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Stamps a key as used. Failures are logged and swallowed: this is bookkeeping only.
|
|
233
|
+
#
|
|
234
|
+
# @param key_id [String] the +key_storage.key_id+ value
|
|
235
|
+
# @return [void]
|
|
236
|
+
def touch_key(key_id)
|
|
237
|
+
with_connection(operation: 'UPDATE_KEY_LAST_USED') do |connection|
|
|
238
|
+
@sql.execute(connection, update_last_used_sql, [Time.now, key_id])
|
|
239
|
+
end
|
|
240
|
+
nil
|
|
241
|
+
rescue StandardError => e
|
|
242
|
+
logger.debug("Failed to update last_used_at for key #{key_id}: #{e.message}")
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Builds a {KeyMetadata} from a +key_storage+ row.
|
|
247
|
+
#
|
|
248
|
+
# @param row [Hash{String => Object}]
|
|
249
|
+
# @return [KeyMetadata]
|
|
250
|
+
def to_key_metadata(row)
|
|
251
|
+
KeyMetadata.new(
|
|
252
|
+
id: row['id']&.to_i,
|
|
253
|
+
key_id: row['key_id'],
|
|
254
|
+
key_name: row['name'],
|
|
255
|
+
master_key_arn: row['master_key_arn'],
|
|
256
|
+
encrypted_data_key: row['encrypted_data_key'],
|
|
257
|
+
hmac_key: @sql.read_binary(row['hmac_key']),
|
|
258
|
+
key_spec: row['key_spec'] || KeyMetadata::DEFAULT_KEY_SPEC,
|
|
259
|
+
created_at: row['created_at'] && to_time(row['created_at']),
|
|
260
|
+
last_used_at: row['last_used_at'] && to_time(row['last_used_at'])
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @return [String] a new identifier for the +key_storage.key_id+ column
|
|
265
|
+
def generate_key_id
|
|
266
|
+
SecureRandom.uuid
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# The cache key for a stored data key. Hashing keeps the encrypted key itself out of
|
|
270
|
+
# the cache keys, which can end up in log messages.
|
|
271
|
+
#
|
|
272
|
+
# @param encrypted_data_key [String]
|
|
273
|
+
# @return [String]
|
|
274
|
+
def data_key_cache_key(encrypted_data_key)
|
|
275
|
+
"#{DATA_KEY_CACHE_PREFIX}#{Base64.strict_encode64(Digest::SHA256.digest(encrypted_data_key.to_s))}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
|
|
280
|
+
def authorize_master_key!(master_key_arn)
|
|
281
|
+
return if @config.master_key_allowed?(master_key_arn)
|
|
282
|
+
|
|
283
|
+
raise Errors::KeyManagementError
|
|
284
|
+
.unauthorized_master_key(
|
|
285
|
+
"The master key is not listed in #{PropertyDefinition::ENCRYPTION_ALLOWED_MASTER_KEY_ARNS.name}; " \
|
|
286
|
+
'refusing to use it'
|
|
287
|
+
)
|
|
288
|
+
.with_master_key_arn(master_key_arn)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def insert_key_sql
|
|
292
|
+
"INSERT INTO #{@schema}.key_storage " \
|
|
293
|
+
'(key_id, name, master_key_arn, encrypted_data_key, hmac_key, key_spec, created_at, last_used_at) ' \
|
|
294
|
+
'VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def select_key_sql
|
|
298
|
+
'SELECT id, key_id, name, master_key_arn, encrypted_data_key, hmac_key, key_spec, created_at, last_used_at ' \
|
|
299
|
+
"FROM #{@schema}.key_storage WHERE id #{eq} ?"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def update_last_used_sql
|
|
303
|
+
"UPDATE #{@schema}.key_storage SET last_used_at = ? WHERE key_id #{eq} ?"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def eq
|
|
307
|
+
@sql.equals_operator
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Runs a KMS call, retrying throttling and transient service errors with exponential
|
|
311
|
+
# backoff and jitter.
|
|
312
|
+
def with_retry(operation)
|
|
313
|
+
attempt = 0
|
|
314
|
+
max_retries = @config.key_management_max_retries
|
|
315
|
+
|
|
316
|
+
loop do
|
|
317
|
+
return yield
|
|
318
|
+
rescue StandardError => e
|
|
319
|
+
raise kms_error(e, operation, attempt, max_retries) if attempt >= max_retries || !retryable?(e)
|
|
320
|
+
|
|
321
|
+
delay = backoff_sec(attempt)
|
|
322
|
+
logger.debug do
|
|
323
|
+
"#{operation} failed (attempt #{attempt + 1}/#{max_retries + 1}), retrying in #{format('%.3f', delay)}s: #{e.message}"
|
|
324
|
+
end
|
|
325
|
+
sleep(delay)
|
|
326
|
+
attempt += 1
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def retryable?(error)
|
|
331
|
+
return true if networking_error?(error)
|
|
332
|
+
return false unless error.respond_to?(:context)
|
|
333
|
+
|
|
334
|
+
status = error.context&.http_response&.status_code
|
|
335
|
+
return true if status && (status >= 500 || status == 429)
|
|
336
|
+
|
|
337
|
+
RETRYABLE_ERROR_CODES.include?(error.class.name.split('::').last)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def networking_error?(error)
|
|
341
|
+
(defined?(Seahorse::Client::NetworkingError) && error.is_a?(Seahorse::Client::NetworkingError)) ||
|
|
342
|
+
error.is_a?(Errno::ECONNREFUSED) || error.is_a?(Errno::ETIMEDOUT) || error.is_a?(Timeout::Error) ||
|
|
343
|
+
error.is_a?(SocketError) || error.is_a?(IOError)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# base * 2**attempt, plus or minus a quarter, at least the base delay.
|
|
347
|
+
def backoff_sec(attempt)
|
|
348
|
+
base_sec = @config.key_management_retry_backoff_base_sec
|
|
349
|
+
exponential = base_sec * (2**attempt)
|
|
350
|
+
jitter = exponential * JITTER_RATIO * ((SecureRandom.random_number * 2) - 1)
|
|
351
|
+
# Not clamp: the configured base delay is allowed to be longer than the cap, and the cap
|
|
352
|
+
# wins when it is.
|
|
353
|
+
delay_sec = [exponential + jitter, base_sec].max
|
|
354
|
+
[delay_sec, MAX_BACKOFF_SEC].min
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def kms_error(error, operation, attempt, max_retries)
|
|
358
|
+
return error if error.is_a?(Errors::KeyManagementError)
|
|
359
|
+
|
|
360
|
+
message = "#{operation} failed: #{error.message}"
|
|
361
|
+
error_class = networking_error?(error) ? :kms_connection_failed : :key_decryption_failed
|
|
362
|
+
Errors::KeyManagementError
|
|
363
|
+
.public_send(error_class, message)
|
|
364
|
+
.with_operation(operation)
|
|
365
|
+
.with_retry_info(attempt + 1, max_retries + 1)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def wrap_storage_error(error, key_id)
|
|
369
|
+
return error if error.is_a?(Errors::KeyManagementError)
|
|
370
|
+
|
|
371
|
+
Errors::KeyManagementError
|
|
372
|
+
.key_storage_failed("Failed to store key metadata: #{error.message}")
|
|
373
|
+
.with_key_id(key_id.to_s)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require_relative 'sanitizer'
|
|
18
|
+
|
|
19
|
+
module AwsAdvancedRubyDriverWrapper
|
|
20
|
+
module Plugins
|
|
21
|
+
module Encryption
|
|
22
|
+
# One row of the +key_storage+ table: an encrypted data key, the master key that wraps
|
|
23
|
+
# it, and the HMAC key used to sign the payloads it encrypts.
|
|
24
|
+
#
|
|
25
|
+
# The data key is only ever held here in its encrypted form. The plaintext key exists
|
|
26
|
+
# only for the duration of a single encrypt or decrypt call and is zeroed afterward.
|
|
27
|
+
KeyMetadata = Data.define(
|
|
28
|
+
:id,
|
|
29
|
+
:key_id,
|
|
30
|
+
:key_name,
|
|
31
|
+
:master_key_arn,
|
|
32
|
+
:encrypted_data_key,
|
|
33
|
+
:hmac_key,
|
|
34
|
+
:key_spec,
|
|
35
|
+
:created_at,
|
|
36
|
+
:last_used_at
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
class KeyMetadata
|
|
40
|
+
DEFAULT_KEY_SPEC = 'AES_256'
|
|
41
|
+
|
|
42
|
+
# @param id [Integer, nil] the +key_storage.id+ surrogate key, nil before the row is inserted
|
|
43
|
+
# @param key_id [String, nil] the +key_storage.key_id+ identifier
|
|
44
|
+
# @param key_name [String, nil] a human-readable name for the key
|
|
45
|
+
# @param master_key_arn [String, nil] the ARN of the KMS master key wrapping the data key
|
|
46
|
+
# @param encrypted_data_key [String, nil] the base64 encoded, KMS encrypted data key
|
|
47
|
+
# @param hmac_key [String, nil] the binary HMAC-SHA256 key used for integrity protection
|
|
48
|
+
# @param key_spec [String] the KMS data key spec, e.g. 'AES_256'
|
|
49
|
+
# @param created_at [Time, nil]
|
|
50
|
+
# @param last_used_at [Time, nil]
|
|
51
|
+
def initialize(id: nil, key_id: nil, key_name: nil, master_key_arn: nil, encrypted_data_key: nil,
|
|
52
|
+
hmac_key: nil, key_spec: DEFAULT_KEY_SPEC, created_at: nil, last_used_at: nil)
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param now [Time]
|
|
57
|
+
# @return [KeyMetadata] a copy stamped with a new last used time
|
|
58
|
+
def with_updated_last_used(now = Time.now)
|
|
59
|
+
with(last_used_at: now)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A key is usable once it names a master key, carries an encrypted data key, and states the
|
|
63
|
+
# key spec it was generated under.
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def valid?
|
|
66
|
+
!blank?(master_key_arn) && !blank?(encrypted_data_key) && !blank?(key_spec)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [String] a description with the key material redacted
|
|
70
|
+
def to_s
|
|
71
|
+
"KeyMetadata{id=#{id.inspect}, key_id=#{key_id.inspect}, key_name=#{key_name.inspect}, " \
|
|
72
|
+
"master_key_arn=#{Sanitizer.arn(master_key_arn).inspect}, encrypted_data_key='[REDACTED]', " \
|
|
73
|
+
"hmac_key=#{hmac_key.nil? ? 'nil' : "'[REDACTED]'"}, key_spec=#{key_spec.inspect}, " \
|
|
74
|
+
"created_at=#{created_at.inspect}, last_used_at=#{last_used_at.inspect}}"
|
|
75
|
+
end
|
|
76
|
+
alias inspect to_s
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def blank?(value)
|
|
81
|
+
value.nil? || value.to_s.strip.empty?
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|