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,146 @@
|
|
|
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 '../../property_definition'
|
|
18
|
+
require_relative 'schema_name'
|
|
19
|
+
|
|
20
|
+
module AwsAdvancedRubyDriverWrapper
|
|
21
|
+
module Plugins
|
|
22
|
+
module Encryption
|
|
23
|
+
# The plugin's own configuration, resolved once from the wrapper properties.
|
|
24
|
+
EncryptionConfig = Data.define(
|
|
25
|
+
:kms_region,
|
|
26
|
+
:kms_endpoint,
|
|
27
|
+
:allowed_master_key_arns,
|
|
28
|
+
:metadata_schema,
|
|
29
|
+
:metadata_cache_enabled,
|
|
30
|
+
:metadata_cache_expiration_sec,
|
|
31
|
+
:metadata_cache_refresh_interval_sec,
|
|
32
|
+
:key_management_max_retries,
|
|
33
|
+
:key_management_retry_backoff_base_sec,
|
|
34
|
+
:audit_logging_enabled,
|
|
35
|
+
:data_key_cache_enabled,
|
|
36
|
+
:data_key_cache_max_size,
|
|
37
|
+
:data_key_cache_expiration_sec,
|
|
38
|
+
:return_unverified_data
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
class EncryptionConfig
|
|
42
|
+
class << self
|
|
43
|
+
# Builds the configuration from the wrapper properties, applying defaults and
|
|
44
|
+
# validating the result.
|
|
45
|
+
#
|
|
46
|
+
# @param props [Concurrent::Map, Hash] the wrapper properties
|
|
47
|
+
# @return [EncryptionConfig]
|
|
48
|
+
# @raise [ArgumentError] if a value is out of range, or if no region was configured
|
|
49
|
+
def from_props(props)
|
|
50
|
+
new(
|
|
51
|
+
kms_region: PropertyDefinition::ENCRYPTION_KMS_REGION.get_string(props) || region_from_env,
|
|
52
|
+
kms_endpoint: PropertyDefinition::ENCRYPTION_KMS_ENDPOINT.get_string(props),
|
|
53
|
+
allowed_master_key_arns: PropertyDefinition::ENCRYPTION_ALLOWED_MASTER_KEY_ARNS.get(props),
|
|
54
|
+
metadata_schema: SchemaName.of(PropertyDefinition::ENCRYPTION_METADATA_SCHEMA.get_string(props)),
|
|
55
|
+
metadata_cache_enabled: PropertyDefinition::ENCRYPTION_METADATA_CACHE_ENABLED.get_bool(props),
|
|
56
|
+
metadata_cache_expiration_sec: PropertyDefinition::ENCRYPTION_METADATA_CACHE_EXPIRATION_SEC.get_float(props),
|
|
57
|
+
metadata_cache_refresh_interval_sec:
|
|
58
|
+
PropertyDefinition::ENCRYPTION_METADATA_CACHE_REFRESH_INTERVAL_SEC.get_float(props),
|
|
59
|
+
key_management_max_retries: PropertyDefinition::ENCRYPTION_KEY_MANAGEMENT_MAX_RETRIES.get_int(props),
|
|
60
|
+
key_management_retry_backoff_base_sec:
|
|
61
|
+
PropertyDefinition::ENCRYPTION_KEY_MANAGEMENT_RETRY_BACKOFF_BASE_SEC.get_float(props),
|
|
62
|
+
audit_logging_enabled: PropertyDefinition::ENCRYPTION_AUDIT_LOGGING_ENABLED.get_bool(props),
|
|
63
|
+
data_key_cache_enabled: PropertyDefinition::ENCRYPTION_DATA_KEY_CACHE_ENABLED.get_bool(props),
|
|
64
|
+
data_key_cache_max_size: PropertyDefinition::ENCRYPTION_DATA_KEY_CACHE_MAX_SIZE.get_int(props),
|
|
65
|
+
data_key_cache_expiration_sec: PropertyDefinition::ENCRYPTION_DATA_KEY_CACHE_EXPIRATION_SEC.get_float(props),
|
|
66
|
+
return_unverified_data: PropertyDefinition::ENCRYPTION_RETURN_UNVERIFIED_DATA.get_bool(props)
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def region_from_env
|
|
73
|
+
ENV.fetch('AWS_REGION', nil) || ENV.fetch('AWS_DEFAULT_REGION', nil)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def initialize(kms_region:, kms_endpoint:, metadata_schema:, metadata_cache_enabled:,
|
|
78
|
+
metadata_cache_expiration_sec:, metadata_cache_refresh_interval_sec:,
|
|
79
|
+
key_management_max_retries:, key_management_retry_backoff_base_sec:,
|
|
80
|
+
audit_logging_enabled:, data_key_cache_enabled:,
|
|
81
|
+
data_key_cache_max_size:, data_key_cache_expiration_sec:, return_unverified_data:,
|
|
82
|
+
allowed_master_key_arns: [])
|
|
83
|
+
super(
|
|
84
|
+
kms_region: kms_region,
|
|
85
|
+
kms_endpoint: kms_endpoint,
|
|
86
|
+
allowed_master_key_arns: normalize_arns(allowed_master_key_arns),
|
|
87
|
+
metadata_schema: SchemaName.of(metadata_schema),
|
|
88
|
+
metadata_cache_enabled: metadata_cache_enabled,
|
|
89
|
+
metadata_cache_expiration_sec: metadata_cache_expiration_sec,
|
|
90
|
+
metadata_cache_refresh_interval_sec: metadata_cache_refresh_interval_sec,
|
|
91
|
+
key_management_max_retries: key_management_max_retries,
|
|
92
|
+
key_management_retry_backoff_base_sec: key_management_retry_backoff_base_sec,
|
|
93
|
+
audit_logging_enabled: audit_logging_enabled,
|
|
94
|
+
data_key_cache_enabled: data_key_cache_enabled,
|
|
95
|
+
data_key_cache_max_size: data_key_cache_max_size,
|
|
96
|
+
data_key_cache_expiration_sec: data_key_cache_expiration_sec,
|
|
97
|
+
return_unverified_data: return_unverified_data
|
|
98
|
+
)
|
|
99
|
+
validate!
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Boolean] true when the metadata should be refreshed on a background thread
|
|
103
|
+
def background_refresh_enabled?
|
|
104
|
+
metadata_cache_enabled && metadata_cache_refresh_interval_sec.positive?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# An empty allow-list (the default) places no restriction on the master keys used - acceptable
|
|
108
|
+
# for the administrative utility, but refused by the plugin itself (see {KmsEncryptionUtility}).
|
|
109
|
+
#
|
|
110
|
+
# @return [Boolean] true when only the listed master keys may be used
|
|
111
|
+
def restricts_master_keys?
|
|
112
|
+
!allowed_master_key_arns.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @param master_key_arn [String, nil] a master key ARN, as recorded in +key_storage+
|
|
116
|
+
# @return [Boolean] true when no restriction is configured, or the key is on the allow-list
|
|
117
|
+
def master_key_allowed?(master_key_arn)
|
|
118
|
+
!restricts_master_keys? || allowed_master_key_arns.include?(master_key_arn)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @return [self]
|
|
122
|
+
# @raise [ArgumentError] if a value is out of range
|
|
123
|
+
def validate!
|
|
124
|
+
raise ArgumentError, 'encryption_kms_region cannot be empty' if kms_region.to_s.strip.empty?
|
|
125
|
+
|
|
126
|
+
PropertyDefinition::ENCRYPTION_METADATA_CACHE_EXPIRATION_SEC.validate!(metadata_cache_expiration_sec)
|
|
127
|
+
PropertyDefinition::ENCRYPTION_METADATA_CACHE_REFRESH_INTERVAL_SEC.validate!(metadata_cache_refresh_interval_sec)
|
|
128
|
+
PropertyDefinition::ENCRYPTION_KEY_MANAGEMENT_MAX_RETRIES.validate!(key_management_max_retries)
|
|
129
|
+
PropertyDefinition::ENCRYPTION_KEY_MANAGEMENT_RETRY_BACKOFF_BASE_SEC.validate!(key_management_retry_backoff_base_sec)
|
|
130
|
+
PropertyDefinition::ENCRYPTION_DATA_KEY_CACHE_MAX_SIZE.validate!(data_key_cache_max_size)
|
|
131
|
+
PropertyDefinition::ENCRYPTION_DATA_KEY_CACHE_EXPIRATION_SEC.validate!(data_key_cache_expiration_sec)
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# Accepts nil, an array, or a comma-separated string, and returns a frozen array of the unique,
|
|
138
|
+
# non-blank entries.
|
|
139
|
+
def normalize_arns(value)
|
|
140
|
+
entries = value.is_a?(String) ? value.split(',') : Array(value)
|
|
141
|
+
entries.map { |arn| arn.to_s.strip }.reject(&:empty?).uniq.freeze
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,391 @@
|
|
|
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 'bigdecimal'
|
|
18
|
+
require 'date'
|
|
19
|
+
require 'openssl'
|
|
20
|
+
require 'securerandom'
|
|
21
|
+
require 'time'
|
|
22
|
+
require_relative 'encryption_algorithm'
|
|
23
|
+
require_relative 'errors'
|
|
24
|
+
require_relative 'type_marker'
|
|
25
|
+
|
|
26
|
+
module AwsAdvancedRubyDriverWrapper
|
|
27
|
+
module Plugins
|
|
28
|
+
module Encryption
|
|
29
|
+
# Encrypts and decrypts single column values with AES-GCM, signed with a separate
|
|
30
|
+
# HMAC-SHA256 key.
|
|
31
|
+
#
|
|
32
|
+
# The payload written to the database is:
|
|
33
|
+
#
|
|
34
|
+
# [ HMAC-SHA256 tag : 32 ][ key id : 4 ][ type marker : 1 ][ GCM IV : 12 ][ ciphertext ][ GCM tag : 16 ]
|
|
35
|
+
#
|
|
36
|
+
# The HMAC covers everything after itself, which lets the database verify that a payload
|
|
37
|
+
# has not been tampered with (see the +verify_encrypted_data_hmac+ SQL function) without
|
|
38
|
+
# ever holding the data key. The key id records which +key_storage+ row the value was
|
|
39
|
+
# encrypted with, so a value stays decryptable after its column's data key has been rotated
|
|
40
|
+
# (the read path resolves that exact key rather than assuming the column's current one); it
|
|
41
|
+
# is inside the HMAC-covered region, so tampering with it is detected. The type marker records
|
|
42
|
+
# how the plaintext was serialized so that the original Ruby type can be recovered on read.
|
|
43
|
+
module EncryptionService
|
|
44
|
+
HMAC_DIGEST = 'SHA256'
|
|
45
|
+
HMAC_TAG_LENGTH = 32
|
|
46
|
+
KEY_ID_LENGTH = 4
|
|
47
|
+
TYPE_MARKER_LENGTH = 1
|
|
48
|
+
GCM_IV_LENGTH = 12
|
|
49
|
+
GCM_TAG_LENGTH = 16
|
|
50
|
+
MIN_ENCRYPTED_LENGTH = HMAC_TAG_LENGTH + KEY_ID_LENGTH + TYPE_MARKER_LENGTH + GCM_IV_LENGTH + GCM_TAG_LENGTH
|
|
51
|
+
|
|
52
|
+
MILLIS_PER_SECOND = 1000.0
|
|
53
|
+
|
|
54
|
+
class << self
|
|
55
|
+
# Encrypts a single value.
|
|
56
|
+
#
|
|
57
|
+
# @param value [Object, nil] the plaintext value; nil is returned unchanged
|
|
58
|
+
# @param data_key [String] the plaintext data key, binary
|
|
59
|
+
# @param hmac_key [String] the HMAC-SHA256 key, binary
|
|
60
|
+
# @param algorithm [String] an {EncryptionAlgorithm} name
|
|
61
|
+
# @param key_id [Integer] the +key_storage+ id of the data key, recorded in the payload so
|
|
62
|
+
# the value stays decryptable after the column's key is rotated
|
|
63
|
+
# @return [String, nil] the binary payload to store, or nil when value is nil
|
|
64
|
+
# @raise [Errors::EncryptionError] if the key or algorithm is unusable, or the cipher fails
|
|
65
|
+
def encrypt(value, data_key, hmac_key, algorithm = EncryptionAlgorithm::DEFAULT, key_id:)
|
|
66
|
+
return nil if value.nil?
|
|
67
|
+
|
|
68
|
+
validate_key!(data_key, algorithm)
|
|
69
|
+
validate_hmac_key!(hmac_key)
|
|
70
|
+
validate_key_id!(key_id)
|
|
71
|
+
|
|
72
|
+
marker = TypeMarker.from_object(value)
|
|
73
|
+
plaintext = serialize_value(value, marker)
|
|
74
|
+
|
|
75
|
+
begin
|
|
76
|
+
# body = [ key id : 4 ][ type marker : 1 ][ IV : 12 ][ ciphertext ][ GCM tag : 16 ]
|
|
77
|
+
body = "#{[key_id].pack('N')}#{seal(plaintext, marker, data_key, algorithm)}".b
|
|
78
|
+
"#{OpenSSL::HMAC.digest(HMAC_DIGEST, hmac_key, body)}#{body}".b
|
|
79
|
+
ensure
|
|
80
|
+
wipe(plaintext)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Reads the key id recorded in a payload, without verifying or decrypting it. Used to pick
|
|
85
|
+
# the key a stored value was written with before its HMAC is checked; a payload with no key
|
|
86
|
+
# id (too short, or written before this format existed) yields nil.
|
|
87
|
+
#
|
|
88
|
+
# @param encrypted [String, nil] the binary payload read from the database
|
|
89
|
+
# @return [Integer, nil]
|
|
90
|
+
def key_id_from_payload(encrypted)
|
|
91
|
+
return nil if encrypted.nil?
|
|
92
|
+
|
|
93
|
+
data = encrypted.b
|
|
94
|
+
return nil if data.bytesize < HMAC_TAG_LENGTH + KEY_ID_LENGTH
|
|
95
|
+
|
|
96
|
+
data.byteslice(HMAC_TAG_LENGTH, KEY_ID_LENGTH).unpack1('N')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Decrypts a payload produced by {encrypt}.
|
|
100
|
+
#
|
|
101
|
+
# @param encrypted [String, nil] the binary payload read from the database
|
|
102
|
+
# @param data_key [String] the plaintext data key, binary
|
|
103
|
+
# @param hmac_key [String] the HMAC-SHA256 key, binary
|
|
104
|
+
# @param algorithm [String] an {EncryptionAlgorithm} name
|
|
105
|
+
# @param target_type [Class, nil] the type to coerce the result to; defaults to the type
|
|
106
|
+
# recorded by the payload's type marker
|
|
107
|
+
# @return [Object, nil] the decrypted value, or nil when encrypted is nil
|
|
108
|
+
# @raise [Errors::EncryptionError] if the payload is malformed, has been tampered with,
|
|
109
|
+
# or cannot be decrypted with the given key
|
|
110
|
+
def decrypt(encrypted, data_key, hmac_key, algorithm = EncryptionAlgorithm::DEFAULT, target_type: nil)
|
|
111
|
+
return nil if encrypted.nil?
|
|
112
|
+
|
|
113
|
+
data = encrypted.b
|
|
114
|
+
if data.bytesize < MIN_ENCRYPTED_LENGTH
|
|
115
|
+
raise Errors::EncryptionError.integrity_check_failed(
|
|
116
|
+
"Encrypted data is too short: #{data.bytesize} bytes, expected at least #{MIN_ENCRYPTED_LENGTH}"
|
|
117
|
+
).with_algorithm(algorithm)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
validate_key!(data_key, algorithm)
|
|
121
|
+
validate_hmac_key!(hmac_key)
|
|
122
|
+
|
|
123
|
+
# The HMAC covers the key id and everything after it; strip the key id once verified to
|
|
124
|
+
# recover the sealed payload the marker, IV, ciphertext and tag live in.
|
|
125
|
+
body = data.byteslice(HMAC_TAG_LENGTH..)
|
|
126
|
+
unless hmac_matches?(data.byteslice(0, HMAC_TAG_LENGTH), body, hmac_key)
|
|
127
|
+
raise Errors::EncryptionError
|
|
128
|
+
.integrity_check_failed('Integrity check failed: the encrypted value has been tampered with')
|
|
129
|
+
.with_algorithm(algorithm)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
payload = body.byteslice(KEY_ID_LENGTH..)
|
|
133
|
+
marker = read_marker(payload)
|
|
134
|
+
plaintext = unseal(payload, data_key, algorithm, marker)
|
|
135
|
+
|
|
136
|
+
begin
|
|
137
|
+
convert_to_target_type(deserialize_value(plaintext, marker), target_type)
|
|
138
|
+
ensure
|
|
139
|
+
wipe(plaintext)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Serializes a value to the bytes that get encrypted.
|
|
144
|
+
#
|
|
145
|
+
# @param value [Object]
|
|
146
|
+
# @param marker [Integer] a {TypeMarker}
|
|
147
|
+
# @return [String] binary
|
|
148
|
+
def serialize_value(value, marker = TypeMarker.from_object(value))
|
|
149
|
+
case marker
|
|
150
|
+
# Ruby has no time-of-day type, so a LOCAL_TIME value is serialized as its own string.
|
|
151
|
+
when TypeMarker::STRING, TypeMarker::GENERIC, TypeMarker::LOCAL_TIME then utf8_bytes(value.to_s)
|
|
152
|
+
when TypeMarker::BYTE_ARRAY then value.b
|
|
153
|
+
when TypeMarker::INTEGER then [value].pack('l>')
|
|
154
|
+
when TypeMarker::LONG then [value].pack('q>')
|
|
155
|
+
when TypeMarker::DOUBLE then [value].pack('G')
|
|
156
|
+
when TypeMarker::FLOAT then [value].pack('g')
|
|
157
|
+
when TypeMarker::BOOLEAN then (value ? 1 : 0).chr
|
|
158
|
+
when TypeMarker::BIG_DECIMAL then value.to_s('F').b
|
|
159
|
+
when TypeMarker::DATE, TypeMarker::TIME, TypeMarker::TIMESTAMP then [to_millis(value)].pack('q>')
|
|
160
|
+
when TypeMarker::LOCAL_DATE then value.strftime('%Y-%m-%d').b
|
|
161
|
+
when TypeMarker::LOCAL_DATE_TIME then value.strftime('%Y-%m-%dT%H:%M:%S').b
|
|
162
|
+
else
|
|
163
|
+
raise Errors::EncryptionError
|
|
164
|
+
.encryption_failed("Unsupported value type: #{value.class}")
|
|
165
|
+
.with_data_type(value.class.to_s)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Turns decrypted bytes back into a Ruby value.
|
|
170
|
+
#
|
|
171
|
+
# Ruby has no time-of-day type, so {TypeMarker::LOCAL_TIME} payloads are returned as
|
|
172
|
+
# their original string.
|
|
173
|
+
#
|
|
174
|
+
# @param bytes [String] binary
|
|
175
|
+
# @param marker [Integer] a {TypeMarker}
|
|
176
|
+
# @return [Object]
|
|
177
|
+
def deserialize_value(bytes, marker)
|
|
178
|
+
case marker
|
|
179
|
+
when TypeMarker::STRING, TypeMarker::GENERIC, TypeMarker::LOCAL_TIME then utf8(bytes)
|
|
180
|
+
when TypeMarker::BYTE_ARRAY then bytes.dup
|
|
181
|
+
when TypeMarker::INTEGER then expect_length(bytes, 4, marker).unpack1('l>')
|
|
182
|
+
when TypeMarker::LONG then expect_length(bytes, 8, marker).unpack1('q>')
|
|
183
|
+
when TypeMarker::DOUBLE then expect_length(bytes, 8, marker).unpack1('G')
|
|
184
|
+
when TypeMarker::FLOAT then expect_length(bytes, 4, marker).unpack1('g')
|
|
185
|
+
when TypeMarker::BOOLEAN then expect_length(bytes, 1, marker).getbyte(0) != 0
|
|
186
|
+
when TypeMarker::BIG_DECIMAL then BigDecimal(utf8(bytes))
|
|
187
|
+
when TypeMarker::DATE then from_millis(expect_length(bytes, 8, marker).unpack1('q>')).to_date
|
|
188
|
+
when TypeMarker::TIME, TypeMarker::TIMESTAMP then from_millis(expect_length(bytes, 8, marker).unpack1('q>'))
|
|
189
|
+
when TypeMarker::LOCAL_DATE then Date.parse(utf8(bytes))
|
|
190
|
+
when TypeMarker::LOCAL_DATE_TIME then DateTime.parse(utf8(bytes))
|
|
191
|
+
else
|
|
192
|
+
raise Errors::EncryptionError
|
|
193
|
+
.decryption_failed("Unsupported type marker: #{marker.inspect}")
|
|
194
|
+
.with_data_type(marker.to_s)
|
|
195
|
+
end
|
|
196
|
+
rescue ArgumentError, TypeError => e
|
|
197
|
+
raise Errors::EncryptionError
|
|
198
|
+
.type_conversion_failed("Failed to deserialize #{TypeMarker.name_for(marker) || marker}: #{e.message}")
|
|
199
|
+
.with_data_type(TypeMarker.name_for(marker))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Coerces a deserialized value to an explicitly requested type.
|
|
203
|
+
#
|
|
204
|
+
# @param value [Object, nil]
|
|
205
|
+
# @param target_type [Class, nil] nil leaves the value as deserialized
|
|
206
|
+
# @return [Object, nil]
|
|
207
|
+
# @raise [Errors::EncryptionError] if the value cannot be coerced
|
|
208
|
+
def convert_to_target_type(value, target_type)
|
|
209
|
+
return value if target_type.nil? || value.nil?
|
|
210
|
+
return value if value.is_a?(target_type)
|
|
211
|
+
|
|
212
|
+
coerce(value, target_type)
|
|
213
|
+
rescue ArgumentError, TypeError => e
|
|
214
|
+
raise Errors::EncryptionError
|
|
215
|
+
.type_conversion_failed("Cannot convert #{value.class} to #{target_type}: #{e.message}")
|
|
216
|
+
.with_data_type(target_type.to_s)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Overwrites a mutable string in place. Best effort: frozen strings are left alone.
|
|
220
|
+
#
|
|
221
|
+
# @param buffer [String, nil]
|
|
222
|
+
# @return [nil]
|
|
223
|
+
def wipe(buffer)
|
|
224
|
+
return nil unless buffer.is_a?(String)
|
|
225
|
+
return nil if buffer.frozen?
|
|
226
|
+
|
|
227
|
+
buffer.replace("\0" * buffer.bytesize)
|
|
228
|
+
nil
|
|
229
|
+
rescue RuntimeError
|
|
230
|
+
# FrozenError is a RuntimeError: a string can be frozen between the check and the
|
|
231
|
+
# replace, and a key that cannot be wiped is not worth failing a query over.
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
# @return [String] the type marker, IV, ciphertext, and GCM tag, without the HMAC
|
|
238
|
+
def seal(plaintext, marker, data_key, algorithm)
|
|
239
|
+
iv = SecureRandom.bytes(GCM_IV_LENGTH)
|
|
240
|
+
cipher = OpenSSL::Cipher.new(EncryptionAlgorithm.cipher_name(algorithm))
|
|
241
|
+
cipher.encrypt
|
|
242
|
+
cipher.key = data_key
|
|
243
|
+
cipher.iv = iv
|
|
244
|
+
"#{marker.chr}#{iv}#{cipher.update(plaintext)}#{cipher.final}#{cipher.auth_tag(GCM_TAG_LENGTH)}"
|
|
245
|
+
rescue OpenSSL::OpenSSLError => e
|
|
246
|
+
raise Errors::EncryptionError
|
|
247
|
+
.encryption_failed("Failed to encrypt value: #{e.message}")
|
|
248
|
+
.with_algorithm(algorithm)
|
|
249
|
+
.with_data_type(TypeMarker.name_for(marker))
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# @return [String] the decrypted plaintext bytes
|
|
253
|
+
def unseal(payload, data_key, algorithm, marker)
|
|
254
|
+
iv = payload.byteslice(TYPE_MARKER_LENGTH, GCM_IV_LENGTH)
|
|
255
|
+
body = payload.byteslice((TYPE_MARKER_LENGTH + GCM_IV_LENGTH)..)
|
|
256
|
+
cipher = OpenSSL::Cipher.new(EncryptionAlgorithm.cipher_name(algorithm))
|
|
257
|
+
cipher.decrypt
|
|
258
|
+
cipher.key = data_key
|
|
259
|
+
cipher.iv = iv
|
|
260
|
+
cipher.auth_tag = body.byteslice(body.bytesize - GCM_TAG_LENGTH, GCM_TAG_LENGTH)
|
|
261
|
+
cipher.update(body.byteslice(0, body.bytesize - GCM_TAG_LENGTH)) + cipher.final
|
|
262
|
+
rescue OpenSSL::OpenSSLError => e
|
|
263
|
+
# The payload's HMAC has already been checked by the time unseal runs, so a GCM failure
|
|
264
|
+
# here means the data key is wrong rather than the data being corrupt. OpenSSL words that
|
|
265
|
+
# failure differently across versions - an empty message on older ones, "AEAD
|
|
266
|
+
# authentication tag verification failed" on newer - so both are reported the same way.
|
|
267
|
+
raise Errors::EncryptionError
|
|
268
|
+
.decryption_failed("Failed to decrypt value: #{gcm_failure_reason(e)}")
|
|
269
|
+
.with_algorithm(algorithm)
|
|
270
|
+
.with_data_type(TypeMarker.name_for(marker))
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# @return [String] the reason to report for an OpenSSL failure during decryption
|
|
274
|
+
def gcm_failure_reason(error)
|
|
275
|
+
message = error.message.to_s
|
|
276
|
+
return message unless message.empty? || message.match?(/auth|tag|verif/i)
|
|
277
|
+
|
|
278
|
+
'the authentication tag does not match this data key'
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def coerce(value, target_type)
|
|
282
|
+
if target_type == String then value.to_s
|
|
283
|
+
elsif target_type == Integer then Integer(value.to_s, 10)
|
|
284
|
+
elsif target_type == Float then Float(value.to_s)
|
|
285
|
+
elsif target_type == BigDecimal then BigDecimal(value.to_s)
|
|
286
|
+
elsif target_type == Date then value.is_a?(Time) ? value.to_date : Date.parse(value.to_s)
|
|
287
|
+
elsif target_type == DateTime then value.is_a?(Time) ? value.to_datetime : DateTime.parse(value.to_s)
|
|
288
|
+
elsif target_type == Time then value.respond_to?(:to_time) ? value.to_time : Time.parse(value.to_s)
|
|
289
|
+
elsif [TrueClass, FalseClass].include?(target_type) then truthy?(value)
|
|
290
|
+
else
|
|
291
|
+
raise Errors::EncryptionError
|
|
292
|
+
.type_conversion_failed("Cannot convert #{value.class} to #{target_type}")
|
|
293
|
+
.with_data_type(target_type.to_s)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def read_marker(payload)
|
|
298
|
+
TypeMarker.from_value(payload.getbyte(0))
|
|
299
|
+
rescue ArgumentError => e
|
|
300
|
+
raise Errors::EncryptionError.decryption_failed(e.message)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def hmac_matches?(expected, payload, hmac_key)
|
|
304
|
+
actual = OpenSSL::HMAC.digest(HMAC_DIGEST, hmac_key, payload)
|
|
305
|
+
OpenSSL.fixed_length_secure_compare(expected, actual)
|
|
306
|
+
rescue ArgumentError
|
|
307
|
+
false
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def validate_key!(data_key, algorithm)
|
|
311
|
+
expected = EncryptionAlgorithm.key_length(algorithm)
|
|
312
|
+
return if data_key.is_a?(String) && data_key.bytesize == expected
|
|
313
|
+
|
|
314
|
+
raise Errors::EncryptionError
|
|
315
|
+
.invalid_key("Data key must be #{expected} bytes for #{algorithm}, got #{data_key&.bytesize.inspect}")
|
|
316
|
+
.with_algorithm(algorithm)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def validate_hmac_key!(hmac_key)
|
|
320
|
+
return if hmac_key.is_a?(String) && !hmac_key.empty?
|
|
321
|
+
|
|
322
|
+
raise Errors::EncryptionError.invalid_key('An HMAC key is required to protect encrypted values')
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# The key id is packed as an unsigned 32-bit integer, so it must be a non-negative Integer
|
|
326
|
+
# that fits in four bytes.
|
|
327
|
+
def validate_key_id!(key_id)
|
|
328
|
+
return if key_id.is_a?(Integer) && key_id >= 0 && key_id <= 0xFFFF_FFFF
|
|
329
|
+
|
|
330
|
+
raise Errors::EncryptionError.encryption_failed(
|
|
331
|
+
"A valid key id is required to tag an encrypted value, got #{key_id.inspect}"
|
|
332
|
+
)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def expect_length(bytes, length, marker)
|
|
336
|
+
return bytes if bytes.bytesize == length
|
|
337
|
+
|
|
338
|
+
raise Errors::EncryptionError
|
|
339
|
+
.decryption_failed("Expected #{length} bytes for #{TypeMarker.name_for(marker)}, got #{bytes.bytesize}")
|
|
340
|
+
.with_data_type(TypeMarker.name_for(marker))
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def utf8(bytes)
|
|
344
|
+
bytes.dup.force_encoding(Encoding::UTF_8)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# The string's text as UTF-8 bytes, which is how {deserialize_value} reads a string back. A
|
|
348
|
+
# string in another encoding is converted first, so that it decrypts to the same text rather
|
|
349
|
+
# than to its own bytes read as UTF-8.
|
|
350
|
+
#
|
|
351
|
+
# A string that cannot be converted is refused rather than stored in a form that would not
|
|
352
|
+
# decrypt to what was written. The error names only the encoding, and is raised without the
|
|
353
|
+
# conversion error as its cause, since that error's message quotes the character it stopped
|
|
354
|
+
# at, which is part of the plaintext.
|
|
355
|
+
#
|
|
356
|
+
# @param string [String]
|
|
357
|
+
# @return [String] binary
|
|
358
|
+
# @raise [Errors::EncryptionError] if the string has no UTF-8 form
|
|
359
|
+
def utf8_bytes(string)
|
|
360
|
+
return string.b if string.encoding == Encoding::UTF_8
|
|
361
|
+
return string.b if string.ascii_only? && string.encoding.ascii_compatible?
|
|
362
|
+
|
|
363
|
+
converted = string.encode(Encoding::UTF_8)
|
|
364
|
+
converted.b
|
|
365
|
+
rescue EncodingError => e
|
|
366
|
+
raise Errors::EncryptionError
|
|
367
|
+
.encryption_failed("Cannot encrypt a #{string.encoding} string as UTF-8 (#{e.class})")
|
|
368
|
+
.with_data_type(string.class.to_s), cause: nil
|
|
369
|
+
ensure
|
|
370
|
+
# The converted copy holds the plaintext too, so it is wiped along with the serialized one.
|
|
371
|
+
wipe(converted)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def to_millis(value)
|
|
375
|
+
(value.to_time.to_f * MILLIS_PER_SECOND).round
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def from_millis(millis)
|
|
379
|
+
Time.at(millis / MILLIS_PER_SECOND)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def truthy?(value)
|
|
383
|
+
return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
384
|
+
|
|
385
|
+
%w[t true y yes 1].include?(value.to_s.strip.downcase)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|