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.
Files changed (150) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +175 -0
  4. data/NOTICE +1 -0
  5. data/README.md +168 -0
  6. data/THIRD-PARTY-LICENSES +473 -0
  7. data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
  9. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
  11. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
  13. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
  17. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
  21. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
  22. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
  24. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
  26. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
  27. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
  28. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
  29. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
  30. data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
  31. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
  32. data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
  33. data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
  34. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
  35. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
  36. data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
  40. data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
  41. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
  42. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
  43. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
  44. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
  45. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
  49. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
  50. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
  52. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
  53. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
  54. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
  55. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
  56. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
  57. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
  59. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
  60. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
  61. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
  62. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
  63. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
  64. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
  66. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
  67. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
  68. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
  69. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
  71. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
  79. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
  80. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
  81. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
  82. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
  83. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
  84. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
  85. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
  86. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
  87. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
  88. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
  89. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
  90. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
  91. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
  92. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
  93. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
  94. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
  95. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
  96. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
  97. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
  98. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
  99. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
  100. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
  101. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
  102. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
  103. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
  104. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
  105. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
  106. data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
  107. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
  108. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
  109. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
  110. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
  111. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
  112. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
  113. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
  114. data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
  115. data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
  116. data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
  117. data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
  118. data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
  119. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
  120. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
  121. data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
  122. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
  123. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
  124. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
  125. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
  126. data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
  127. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
  128. data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
  129. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
  130. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
  131. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
  132. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
  133. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
  134. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
  135. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
  136. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
  137. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
  138. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
  139. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
  140. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
  141. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
  142. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
  143. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
  144. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
  145. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
  146. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
  147. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
  148. data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
  149. data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
  150. 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