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,159 @@
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 'encryption_service'
18
+ require_relative 'errors'
19
+
20
+ module AwsAdvancedRubyDriverWrapper
21
+ module Plugins
22
+ module Encryption
23
+ # Encrypts and decrypts single column values for the duration of one call.
24
+ #
25
+ # A cipher is created for each intercepted call and {#release}d when the call returns. That
26
+ # scope is what keeps plaintext data keys short lived: a key is decrypted at most once per
27
+ # column per call, however many values that call touches, and every copy is zeroed again on
28
+ # release.
29
+ #
30
+ # Values are returned as strings, matching what both drivers hand back for a text column.
31
+ class ColumnCipher
32
+ # @param key_manager [KeyManager]
33
+ # @param sql_runner [SqlRunner] used to read a bytea or blob column into binary data
34
+ def initialize(key_manager:, sql_runner:)
35
+ @key_manager = key_manager
36
+ @sql = sql_runner
37
+ @data_keys = {}
38
+ # Key material resolved by the id embedded in a value, cached for the cipher's lifetime so
39
+ # a result full of rows written under a rotated-away key costs one lookup, not one per row.
40
+ @key_metadata_by_id = {}
41
+ end
42
+
43
+ # Encrypts one value for the column the configuration describes.
44
+ #
45
+ # @param value [Object, nil] the plaintext value
46
+ # @param config [ColumnEncryptionConfig] the column's kms_encryption configuration
47
+ # @return [String, nil] the binary payload to store, nil when value is nil
48
+ # @raise [Errors::EncryptionError] if the value cannot be encrypted
49
+ def encrypt(value, config)
50
+ return nil if value.nil?
51
+
52
+ metadata = key_metadata!(config)
53
+ EncryptionService.encrypt(
54
+ value, data_key_for(metadata), hmac_key_for(metadata, config), config.algorithm, key_id: metadata.id
55
+ )
56
+ end
57
+
58
+ # Decrypts one value read from the column the configuration describes.
59
+ #
60
+ # The read fails closed: every value of a column configured for kms_encryption is expected to
61
+ # be an encrypted payload, so a value that cannot be verified and decrypted - too short to be
62
+ # a payload, an integrity tag that does not match, or a stored key with no HMAC key - is
63
+ # raised on rather than returned, so the wrapper never hands the application a value it cannot
64
+ # vouch for. A null column value reads back as nil.
65
+ #
66
+ # @param raw [Object, nil] the raw column value
67
+ # @param config [ColumnEncryptionConfig] the column's kms_encryption configuration
68
+ # @return [Object, nil] the decrypted value, or nil when the column value is null
69
+ # @raise [Errors::EncryptionError] if the value cannot be verified and decrypted, is not the
70
+ # stored bytes of an encrypted column, or the column has no usable key material
71
+ def decrypt(raw, config)
72
+ return nil if raw.nil?
73
+
74
+ # A value read from an encrypted (binary) column is a String; nil, handled above, is the
75
+ # only other shape a NULL produces. Any other type is not the stored bytes of an encrypted
76
+ # column, so it is refused rather than returned unverified.
77
+ raise Errors::EncryptionError.decryption_failed("Cannot decrypt a #{raw.class} value") unless raw.is_a?(String)
78
+
79
+ # A column configured for kms_encryption must have key material; its absence is a
80
+ # misconfiguration worth failing on, as on the encrypt side.
81
+ key_metadata!(config)
82
+
83
+ bytes = @sql.read_binary(raw)
84
+ # Resolve the key the value was written with from the id in its payload, so a value written
85
+ # before a key rotation still decrypts. A value whose key cannot be resolved falls back to
86
+ # the current key, against which it then fails its integrity check and is refused.
87
+ metadata = key_metadata_for_value(bytes, config)
88
+ hmac_key = hmac_key_for(metadata, config)
89
+
90
+ EncryptionService.decrypt(bytes, data_key_for(metadata), hmac_key, config.algorithm, target_type: String)
91
+ end
92
+
93
+ # Zeroes every plaintext data key this cipher decrypted.
94
+ # @return [void]
95
+ def release
96
+ @data_keys.each_value { |data_key| EncryptionService.wipe(data_key) }
97
+ @data_keys.clear
98
+ nil
99
+ end
100
+
101
+ private
102
+
103
+ # The key a stored value was written with. The payload records the +key_storage+ id, so a
104
+ # value keeps decrypting after its column's key has been rotated: the current key is used
105
+ # when the ids match (the common case, and the only one that needs no extra lookup), and any
106
+ # other id is fetched from +key_storage+ and cached. A value with no embedded id (legacy
107
+ # data) or one whose key is gone falls back to the current key, against which it then fails
108
+ # its integrity check and is refused by {#decrypt}.
109
+ #
110
+ # @return [KeyMetadata, nil]
111
+ def key_metadata_for_value(bytes, config)
112
+ current = config.key_metadata
113
+ key_id = EncryptionService.key_id_from_payload(bytes)
114
+ return current if key_id.nil? || (current && current.id == key_id)
115
+
116
+ @key_metadata_by_id[key_id] ||= resolve_key_metadata(key_id) || current
117
+ end
118
+
119
+ # Looks a key up by the id embedded in a value. The lookup is opportunistic: a value that is
120
+ # not really an encrypted payload (legacy data, tampered bytes) can carry an arbitrary id, and
121
+ # a value written before this format carries none, so a lookup that finds nothing or fails must
122
+ # not surface as an error - the caller falls back to the current key, against which the value
123
+ # then fails its integrity check and is refused by {#decrypt}.
124
+ def resolve_key_metadata(key_id)
125
+ @key_manager.key_metadata_by_id(key_id)
126
+ rescue Errors::EncryptionPluginError
127
+ nil
128
+ end
129
+
130
+ def data_key_for(metadata)
131
+ @data_keys[metadata.encrypted_data_key] ||=
132
+ @key_manager.decrypt_data_key(metadata.encrypted_data_key, metadata.master_key_arn)
133
+ end
134
+
135
+ def hmac_key_for(metadata, config)
136
+ hmac_key = metadata.hmac_key
137
+ return hmac_key unless hmac_key.nil? || hmac_key.empty?
138
+
139
+ raise Errors::EncryptionError
140
+ .invalid_key('The stored key has no HMAC key')
141
+ .with_table(config.table_name)
142
+ .with_column(config.column_name)
143
+ end
144
+
145
+ def key_metadata!(config)
146
+ metadata = config.key_metadata
147
+ if metadata.nil?
148
+ raise Errors::EncryptionError
149
+ .invalid_key('The column has no key material')
150
+ .with_table(config.table_name)
151
+ .with_column(config.column_name)
152
+ end
153
+
154
+ metadata
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,61 @@
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 'encryption_algorithm'
18
+ require_relative 'key_metadata'
19
+
20
+ module AwsAdvancedRubyDriverWrapper
21
+ module Plugins
22
+ module Encryption
23
+ # One row of the +encryption_metadata+ table joined with the key it points at: the
24
+ # algorithm to use for a single table column, and the key material to use with it.
25
+ ColumnEncryptionConfig = Data.define(
26
+ :table_name,
27
+ :column_name,
28
+ :algorithm,
29
+ :key_id,
30
+ :key_metadata,
31
+ :created_at,
32
+ :updated_at
33
+ )
34
+
35
+ class ColumnEncryptionConfig
36
+ # @param table_name [String]
37
+ # @param column_name [String]
38
+ # @param algorithm [String] an {EncryptionAlgorithm} name
39
+ # @param key_id [Integer, nil] the +encryption_metadata.key_id+ foreign key
40
+ # @param key_metadata [KeyMetadata, nil] the joined +key_storage+ row
41
+ # @param created_at [Time, nil]
42
+ # @param updated_at [Time, nil]
43
+ def initialize(table_name:, column_name:, algorithm: EncryptionAlgorithm::DEFAULT, key_id: nil,
44
+ key_metadata: nil, created_at: nil, updated_at: nil)
45
+ super
46
+ end
47
+
48
+ # The cache key for this column, e.g. 'users.ssn'.
49
+ # @return [String]
50
+ def column_identifier
51
+ "#{table_name}.#{column_name}"
52
+ end
53
+
54
+ # @return [Boolean] true when the column has key material and a supported algorithm
55
+ def usable?
56
+ EncryptionAlgorithm.supported?(algorithm) && !key_metadata.nil? && key_metadata.valid?
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,91 @@
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 '../../errors'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module Plugins
21
+ module Encryption
22
+ # Gives the encryption components (KeyManager, MetadataManager, KeyManagementUtility) a
23
+ # connection to run a statement on. It is supplied one of two ways, and exactly one must be set:
24
+ #
25
+ # * +connection+ - a caller-owned connection, reused for every operation and never closed here.
26
+ # This is how {KeyManagementUtility} wires things for a user with a connection in hand.
27
+ # * +service_container+ - the plugin's runtime source: each call opens a short-lived connection
28
+ # of its own through the wrapper's connect pipeline and closes it afterward, so metadata and
29
+ # key reads never ride the application's connection.
30
+ module ConnectionSource
31
+ # Records the connection source from a constructor. Exactly one argument must be non-nil.
32
+ #
33
+ # @raise [ArgumentError] unless exactly one of the two is given
34
+ def use_connection_source(connection:, service_container:)
35
+ if [connection, service_container].compact.size != 1
36
+ raise ArgumentError, 'provide exactly one of connection: or service_container:'
37
+ end
38
+
39
+ @connection = connection
40
+ @service_container = service_container
41
+ end
42
+
43
+ # Yields a usable connection. A supplied connection is yielded as-is and left open; otherwise a
44
+ # short-lived connection is opened for the call and closed afterward.
45
+ #
46
+ # @param operation [String, nil] operation name, for error context
47
+ # @yieldparam connection [Object] a pg or mysql2 connection
48
+ # @return [Object] whatever the block returns
49
+ # @raise [Errors::AwsError] if a runtime connection cannot be opened
50
+ def with_connection(operation: nil)
51
+ return yield(@connection) if @connection
52
+
53
+ connection = open_runtime_connection(operation)
54
+ begin
55
+ yield connection
56
+ ensure
57
+ close_runtime_connection(connection)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Opens a short-lived connection through the connect pipeline, the way the plugin's runtime
64
+ # reads its metadata and keys (independent of the application's connection).
65
+ def open_runtime_connection(operation)
66
+ connection_service = @service_container.connection_service
67
+ connection = @service_container.plugin_manager.internal_connect(
68
+ connection_service.current_host_info,
69
+ connection_service.driver_props.dup,
70
+ connection_service.wrapper_props,
71
+ false
72
+ )
73
+ if connection.nil?
74
+ raise AwsAdvancedRubyDriverWrapper::Errors::AwsError,
75
+ "The connect pipeline returned no connection for #{operation || 'a metadata query'}"
76
+ end
77
+
78
+ connection
79
+ end
80
+
81
+ def close_runtime_connection(connection)
82
+ return if connection.nil?
83
+
84
+ @service_container.dialect_service.driver_dialect.close_connection(connection)
85
+ rescue StandardError
86
+ nil
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,220 @@
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 '../../logging'
18
+ require_relative 'encryption_service'
19
+
20
+ module AwsAdvancedRubyDriverWrapper
21
+ module Plugins
22
+ module Encryption
23
+ # An in-memory cache of plaintext data keys, so that reading an encrypted column does
24
+ # not need a KMS Decrypt call per statement.
25
+ #
26
+ # This deliberately does not use the shared StorageService: data keys need a bounded
27
+ # size with oldest-first eviction, their bytes have to be zeroed when they leave the
28
+ # cache, and every entry is handed out as a copy so that a caller wiping its own copy
29
+ # cannot corrupt the cached one.
30
+ class DataKeyCache
31
+ include Logging
32
+
33
+ MIN_CLEANUP_INTERVAL_SEC = 30
34
+ CLEANUP_INTERVAL_DIVISOR = 4
35
+
36
+ # A cached plaintext data key. The stored string is private to the cache.
37
+ CacheEntry = Data.define(:data_key, :created_at) do
38
+ def expired?(ttl_sec, now = Process.clock_gettime(Process::CLOCK_MONOTONIC))
39
+ now >= created_at + ttl_sec
40
+ end
41
+
42
+ # Redact the plaintext data key so it is never exposed if an instance is
43
+ # logged, interpolated, or rendered in a backtrace.
44
+ def inspect
45
+ "#<data CacheEntry data_key=#{AwsAdvancedRubyDriverWrapper::REDACTED.inspect}, created_at=#{created_at.inspect}>"
46
+ end
47
+ alias_method :to_s, :inspect
48
+
49
+ # `pp` / PrettyPrint does not call #inspect; route them through the
50
+ # redacted representation so `pp entry` cannot leak the data key.
51
+ def pretty_print(pp)
52
+ pp.text(inspect)
53
+ end
54
+ end
55
+
56
+ # A point-in-time snapshot of the cache counters.
57
+ CacheStats = Data.define(:size, :hits, :misses, :evictions) do
58
+ # @return [Float] the percentage of lookups that were served from the cache
59
+ def hit_rate
60
+ total = hits + misses
61
+ total.zero? ? 0.0 : (hits.to_f / total) * 100
62
+ end
63
+
64
+ def to_s
65
+ format('CacheStats{size=%<size>d, hits=%<hits>d, misses=%<misses>d, evictions=%<evictions>d, ' \
66
+ 'hit_rate=%<hit_rate>.2f%%}',
67
+ size: size, hits: hits, misses: misses, evictions: evictions, hit_rate: hit_rate)
68
+ end
69
+ end
70
+
71
+ # @param max_size [Integer] the maximum number of data keys held at once
72
+ # @param ttl_sec [Numeric] how long a data key stays cached, in seconds
73
+ # @param enabled [Boolean] when false every lookup misses and nothing is stored
74
+ def initialize(max_size:, ttl_sec:, enabled: true)
75
+ @max_size = max_size
76
+ @ttl_sec = ttl_sec
77
+ @enabled = enabled
78
+ @cache = {}
79
+ @lock = Mutex.new
80
+ @hits = 0
81
+ @misses = 0
82
+ @evictions = 0
83
+ @running = enabled
84
+ @cleanup_thread = start_cleanup_thread if enabled
85
+ end
86
+
87
+ # @return [Boolean]
88
+ def enabled?
89
+ @enabled
90
+ end
91
+
92
+ # @param key [String] the cached data key, see {KeyManager#data_key_cache_key}
93
+ # @return [String, nil] a copy of the cached data key, or nil on a miss
94
+ def get(key)
95
+ return nil unless @enabled
96
+
97
+ @lock.synchronize do
98
+ entry = @cache[key]
99
+
100
+ if entry.nil?
101
+ @misses += 1
102
+ nil
103
+ elsif entry.expired?(@ttl_sec)
104
+ @cache.delete(key)
105
+ EncryptionService.wipe(entry.data_key)
106
+ @misses += 1
107
+ nil
108
+ else
109
+ @hits += 1
110
+ entry.data_key.dup
111
+ end
112
+ end
113
+ end
114
+
115
+ # Stores a copy of the given data key, evicting the oldest entry first when full.
116
+ #
117
+ # @param key [String]
118
+ # @param data_key [String] the plaintext data key; the caller keeps ownership of it
119
+ # @return [void]
120
+ def put(key, data_key)
121
+ return unless @enabled && data_key
122
+
123
+ @lock.synchronize do
124
+ existing = @cache.delete(key)
125
+ EncryptionService.wipe(existing.data_key) if existing
126
+ evict_oldest if @cache.size >= @max_size
127
+
128
+ @cache[key] = CacheEntry.new(data_key: data_key.dup, created_at: Process.clock_gettime(Process::CLOCK_MONOTONIC))
129
+ end
130
+ end
131
+
132
+ # @param key [String]
133
+ # @return [void]
134
+ def remove(key)
135
+ @lock.synchronize do
136
+ entry = @cache.delete(key)
137
+ EncryptionService.wipe(entry.data_key) if entry
138
+ end
139
+ end
140
+
141
+ # Zeroes and drops every cached data key.
142
+ # @return [void]
143
+ def clear
144
+ @lock.synchronize do
145
+ @cache.each_value { |entry| EncryptionService.wipe(entry.data_key) }
146
+ @cache.clear
147
+ end
148
+ end
149
+
150
+ # Drops every expired entry. Called on a schedule by the cleanup thread.
151
+ # @return [Integer] the number of entries removed
152
+ def remove_expired_entries
153
+ @lock.synchronize do
154
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
155
+ expired = @cache.select { |_, entry| entry.expired?(@ttl_sec, now) }
156
+ expired.each_key do |key|
157
+ EncryptionService.wipe(@cache.delete(key).data_key)
158
+ end
159
+ expired.size
160
+ end
161
+ end
162
+
163
+ # @return [Integer]
164
+ def size
165
+ @lock.synchronize { @cache.size }
166
+ end
167
+
168
+ # @return [CacheStats]
169
+ def stats
170
+ @lock.synchronize { CacheStats.new(size: @cache.size, hits: @hits, misses: @misses, evictions: @evictions) }
171
+ end
172
+
173
+ # Stops the cleanup thread and zeroes every cached key.
174
+ # @return [void]
175
+ def shutdown
176
+ @running = false
177
+ begin
178
+ @cleanup_thread&.wakeup
179
+ rescue ThreadError
180
+ nil
181
+ end
182
+ @cleanup_thread&.join(5)
183
+ clear
184
+ end
185
+
186
+ private
187
+
188
+ # Removes the entry that has been cached the longest. Callers already hold the lock.
189
+ def evict_oldest
190
+ oldest_key, oldest_entry = @cache.min_by { |_, entry| entry.created_at }
191
+ return if oldest_key.nil?
192
+
193
+ @cache.delete(oldest_key)
194
+ EncryptionService.wipe(oldest_entry.data_key)
195
+ @evictions += 1
196
+ end
197
+
198
+ def cleanup_interval_sec
199
+ [@ttl_sec.to_f / CLEANUP_INTERVAL_DIVISOR, MIN_CLEANUP_INTERVAL_SEC].max
200
+ end
201
+
202
+ def start_cleanup_thread
203
+ interval = cleanup_interval_sec
204
+ thread = Thread.new do
205
+ while @running
206
+ sleep(interval)
207
+ begin
208
+ remove_expired_entries if @running
209
+ rescue StandardError => e
210
+ logger.debug("DataKeyCache cleanup failed: #{e.message}")
211
+ end
212
+ end
213
+ end
214
+ thread.name = 'kms_encryption-data-key-cache-cleanup'
215
+ thread
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,75 @@
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 'errors'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module Plugins
21
+ module Encryption
22
+ # The symmetric algorithms the plugin can use to encrypt column values. The names are
23
+ # the ones stored in the +encryption_metadata.encryption_algorithm+ column.
24
+ module EncryptionAlgorithm
25
+ AES_256_GCM = 'AES-256-GCM'
26
+ AES_128_GCM = 'AES-128-GCM'
27
+ DEFAULT = AES_256_GCM
28
+
29
+ # Expected data key length in bytes for each algorithm.
30
+ KEY_LENGTHS = {
31
+ AES_256_GCM => 32,
32
+ AES_128_GCM => 16
33
+ }.freeze
34
+
35
+ # OpenSSL cipher names for each algorithm.
36
+ CIPHERS = {
37
+ AES_256_GCM => 'aes-256-gcm',
38
+ AES_128_GCM => 'aes-128-gcm'
39
+ }.freeze
40
+
41
+ ALL = KEY_LENGTHS.keys.freeze
42
+
43
+ module_function
44
+
45
+ # @param name [String] an algorithm name, e.g. 'AES-256-GCM'
46
+ # @return [Integer] the required data key length in bytes
47
+ # @raise [Errors::EncryptionError] if the algorithm is not supported
48
+ def key_length(name)
49
+ KEY_LENGTHS.fetch(name) { raise unsupported(name) }
50
+ end
51
+
52
+ # @param name [String] an algorithm name, e.g. 'AES-256-GCM'
53
+ # @return [String] the OpenSSL cipher name
54
+ # @raise [Errors::EncryptionError] if the algorithm is not supported
55
+ def cipher_name(name)
56
+ CIPHERS.fetch(name) { raise unsupported(name) }
57
+ end
58
+
59
+ # @param name [String, nil]
60
+ # @return [Boolean]
61
+ def supported?(name)
62
+ KEY_LENGTHS.key?(name)
63
+ end
64
+
65
+ # @param name [String, nil]
66
+ # @return [Errors::EncryptionError]
67
+ def unsupported(name)
68
+ Errors::EncryptionError
69
+ .invalid_algorithm("Unsupported kms_encryption algorithm: #{name.inspect}. Supported algorithms: #{ALL.join(', ')}")
70
+ .with_algorithm(name)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end