aws-sdk-resources 2.0.14.pre → 2.0.15.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/aws-sdk-resources/services/s3.rb +1 -0
- data/lib/aws-sdk-resources/services/s3/encryption.rb +18 -0
- data/lib/aws-sdk-resources/services/s3/encryption/client.rb +276 -0
- data/lib/aws-sdk-resources/services/s3/encryption/config.rb +65 -0
- data/lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb +84 -0
- data/lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb +38 -0
- data/lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb +71 -0
- data/lib/aws-sdk-resources/services/s3/encryption/errors.rb +13 -0
- data/lib/aws-sdk-resources/services/s3/encryption/io_decrypter.rb +36 -0
- data/lib/aws-sdk-resources/services/s3/encryption/io_encrypter.rb +69 -0
- data/lib/aws-sdk-resources/services/s3/encryption/key_provider.rb +29 -0
- data/lib/aws-sdk-resources/services/s3/encryption/materials.rb +67 -0
- data/lib/aws-sdk-resources/services/s3/encryption/utils.rb +80 -0
- metadata +16 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f052a7ea17d6dae941c7488668075d7faecf9767
|
4
|
+
data.tar.gz: cae90c630414ec0552c9b8e4df95449222d048e7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc04753ba36090c1c4f521b2c2648d1bfd0f8cfa3071c15cd2ae21ec828cda120aa31c08dfd72941a20e71c12140773ce15fd8b124eb32e2b8e5f1e049a69212
|
7
|
+
data.tar.gz: 901efaa0baa5c3f8e52871c4ec57c963400c0cf1ebed22ec8f59d34f2cc2fe2d5d4cbfde9fc34af7da5639656e0da834a78723ce3efca06a8c5c16484c71c824
|
@@ -6,6 +6,7 @@ module Aws
|
|
6
6
|
require 'aws-sdk-resources/services/s3/multipart_upload'
|
7
7
|
require 'aws-sdk-resources/services/s3/public_url'
|
8
8
|
|
9
|
+
autoload :Encryption, 'aws-sdk-resources/services/s3/encryption'
|
9
10
|
autoload :FilePart, 'aws-sdk-resources/services/s3/file_part'
|
10
11
|
autoload :FileUploader, 'aws-sdk-resources/services/s3/file_uploader'
|
11
12
|
autoload :MultipartFileUploader, 'aws-sdk-resources/services/s3/multipart_file_uploader'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Aws
|
2
|
+
module S3
|
3
|
+
module Encryption
|
4
|
+
|
5
|
+
autoload :Client, 'aws-sdk-resources/services/s3/encryption/client'
|
6
|
+
autoload :DecryptHandler, 'aws-sdk-resources/services/s3/encryption/decrypt_handler'
|
7
|
+
autoload :DefaultKeyProvider, 'aws-sdk-resources/services/s3/encryption/default_key_provider'
|
8
|
+
autoload :EncryptHandler, 'aws-sdk-resources/services/s3/encryption/encrypt_handler'
|
9
|
+
autoload :Errors, 'aws-sdk-resources/services/s3/encryption/errors'
|
10
|
+
autoload :IOEncrypter, 'aws-sdk-resources/services/s3/encryption/io_encrypter'
|
11
|
+
autoload :IODecrypter, 'aws-sdk-resources/services/s3/encryption/io_decrypter'
|
12
|
+
autoload :KeyProvider, 'aws-sdk-resources/services/s3/encryption/key_provider'
|
13
|
+
autoload :Materials, 'aws-sdk-resources/services/s3/encryption/materials'
|
14
|
+
autoload :Utils, 'aws-sdk-resources/services/s3/encryption/utils'
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
module Aws
|
2
|
+
module S3
|
3
|
+
|
4
|
+
# Provides an encryption client that encrypts and decrypts data client-side,
|
5
|
+
# storing the encrypted data in Amazon S3.
|
6
|
+
#
|
7
|
+
# This client uses a process called "envelope encryption". Your private
|
8
|
+
# encryption keys and your data's plain-text are **never** sent to
|
9
|
+
# Amazon S3. **If you loose you encrption keys, you will not be able to
|
10
|
+
# un-encrypt your data.**
|
11
|
+
#
|
12
|
+
# ## Envelope Encryption Overview
|
13
|
+
#
|
14
|
+
# The goal of envelope encryption is to combine the performance of
|
15
|
+
# fast symmetric encryption while maintaining the secure key management
|
16
|
+
# that asymmetric keys provide.
|
17
|
+
#
|
18
|
+
# A one-time-use symmetric key (envelope key) is generated client-side.
|
19
|
+
# This is used to encrypt the data client-side. This key is then
|
20
|
+
# encrypted by your master key and stored alongside your data in Amazon
|
21
|
+
# S3.
|
22
|
+
#
|
23
|
+
# When accessing your encrypted data with the encryption client,
|
24
|
+
# the encrypted envelope key is retrieved and decrypted client-side
|
25
|
+
# with your master key. The envelope key is then used to decrypt the
|
26
|
+
# data client-side.
|
27
|
+
#
|
28
|
+
# One of the benefits of envelope encryption is that if your master key
|
29
|
+
# is compromised, you have the option of jut re-encrypting the stored
|
30
|
+
# envelope symmetric keys, instead of re-encrypting all of the
|
31
|
+
# data in your account.
|
32
|
+
#
|
33
|
+
# ## Basic Usage
|
34
|
+
#
|
35
|
+
# The encryption client requires an {Aws::S3::Client}. If you do not
|
36
|
+
# provide a `:client`, then a client will be constructed for you.
|
37
|
+
#
|
38
|
+
# require 'openssl'
|
39
|
+
# key = OpenSSL::PKey::RSA.new(1024)
|
40
|
+
#
|
41
|
+
# # encryption client
|
42
|
+
# s3 = Aws::S3::Encryption::Client.new(encryption_key: key)
|
43
|
+
#
|
44
|
+
# # round-trip an object, encrypted/decrypted locally
|
45
|
+
# s3.put_object(bucket:'aws-sdk', key:'secret', body:'handshake')
|
46
|
+
# s3.get_object(bucket:'aws-sdk', key:'secret').body.read
|
47
|
+
# #=> 'handshake'
|
48
|
+
#
|
49
|
+
# # reading encrypted object without the encryption client
|
50
|
+
# # results in the getting the cipher text
|
51
|
+
# Aws::S3::Client.new.get_object(bucket:'aws-sdk', key:'secret').body.read
|
52
|
+
# #=> "... cipher text ..."
|
53
|
+
#
|
54
|
+
# ## Keys
|
55
|
+
#
|
56
|
+
# For client-side encryption to work, you must provide an encryption key:
|
57
|
+
#
|
58
|
+
# key = OpenSSL::Cipher.new("AES-256-ECB").random_key # symmetric key
|
59
|
+
# key = OpenSSL::PKey::RSA.new(1024) # asymmetric key pair
|
60
|
+
#
|
61
|
+
# s3 = Aws::S3::Encryption::Client.new(encryption_key: key)
|
62
|
+
#
|
63
|
+
# Alternatively, you can use a {KeyProvider}. A key provider makes
|
64
|
+
# it easy to work with multiple keys and simplifies key rotation.
|
65
|
+
#
|
66
|
+
# ### Key Provider
|
67
|
+
#
|
68
|
+
# A {KeyProvider} is any object that responds to:
|
69
|
+
#
|
70
|
+
# * `#encryption_materials`
|
71
|
+
# * `#key_for(materials_description)`
|
72
|
+
#
|
73
|
+
# Here is a trivial implementation of an in-memory key provider.
|
74
|
+
# This is provided as a demonstration of the key provider interface,
|
75
|
+
# and should not be used in production:
|
76
|
+
#
|
77
|
+
# class KeyProvider
|
78
|
+
#
|
79
|
+
# def initialize(default_key_name, keys)
|
80
|
+
# @keys = keys
|
81
|
+
# @encryption_materials = Aws::S3::Encryption::Materials.new(
|
82
|
+
# key: @keys[default_key_name],
|
83
|
+
# description: MultiJson.dump(key: default_key_name),
|
84
|
+
# )
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# attr_reader :encryption_materials
|
88
|
+
#
|
89
|
+
# def key_for(matdesc)
|
90
|
+
# key_name = MultiJson.load(matdesc)['key']
|
91
|
+
# if key = @keys[key_name]
|
92
|
+
# key
|
93
|
+
# else
|
94
|
+
# raise "encryption key not found for: #{matdesc.inspect}"
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# Given the above key provider, you can create an encryption client that
|
100
|
+
# chooses the key to use based on the materials description stored with
|
101
|
+
# the encrypted object. This makes it possible to use multiple keys
|
102
|
+
# and simplifies key rotation.
|
103
|
+
#
|
104
|
+
# # uses "new-key" for encrypting objects, uses either for decrypting
|
105
|
+
# keys = KeyProvider.new('new-key', {
|
106
|
+
# "old-key" => Base64.decode64("kM5UVbhE/4rtMZJfsadYEdm2vaKFsmV2f5+URSeUCV4="),
|
107
|
+
# "new-key" => Base64.decode64("w1WLio3agRWRTSJK/Ouh8NHoqRQ6fn5WbSXDTHjXMSo="),
|
108
|
+
# }),
|
109
|
+
#
|
110
|
+
# # chooses the key based on the materials description stored
|
111
|
+
# # with the encrypted object
|
112
|
+
# s3 = Aws::S3::Encryption::Client.new(key_provider: keys)
|
113
|
+
#
|
114
|
+
# ## Materials Description
|
115
|
+
#
|
116
|
+
# A materials description is JSON document string that is stored
|
117
|
+
# in the metadata (or instruction file) of an encrypted object.
|
118
|
+
# The {DefaultKeyProvider} uses the empty JSON document `"{}"`.
|
119
|
+
#
|
120
|
+
# When building a key provider, you are free to store whatever
|
121
|
+
# information you need to identify the master key that was used
|
122
|
+
# to encrypt the object.
|
123
|
+
#
|
124
|
+
# ## Envelope Location
|
125
|
+
#
|
126
|
+
# By default, the encryption client store the encryption envelope
|
127
|
+
# with the object, as metadata. You can choose to have the envelope
|
128
|
+
# stored in a separate "instruction file". An instruction file
|
129
|
+
# is an object, with the key of the encrypted object, suffixed with
|
130
|
+
# `".instruction"`.
|
131
|
+
#
|
132
|
+
# Specify the `:envelope_location` option as `:instruction_file` to
|
133
|
+
# use an instruction file for storing the envelope.
|
134
|
+
#
|
135
|
+
# # default behavior
|
136
|
+
# s3 = Aws::S3::Encryption::Client.new(
|
137
|
+
# key_provider: ...,
|
138
|
+
# envelope_location: :metadata,
|
139
|
+
# )
|
140
|
+
#
|
141
|
+
# # store envelope in a separate object
|
142
|
+
# s3 = Aws::S3::Encryption::Client.new(
|
143
|
+
# key_provider: ...,
|
144
|
+
# envelope_location: :instruction_file,
|
145
|
+
# instruction_file_suffix: '.instruction' # default
|
146
|
+
# )
|
147
|
+
#
|
148
|
+
# When using an instruction file, multiple requests are made when
|
149
|
+
# putting and getting the object. **This may cause issues if you are
|
150
|
+
# issuing concurrent PUT and GET requests to an encrypted object.**
|
151
|
+
#
|
152
|
+
module Encryption
|
153
|
+
|
154
|
+
class Client
|
155
|
+
|
156
|
+
# Creates a new encryption client. You must provide on of the following
|
157
|
+
# options:
|
158
|
+
#
|
159
|
+
# * `:key_provider`
|
160
|
+
# * `:encryption_key`
|
161
|
+
#
|
162
|
+
# @option opitons [S3::Client] :client A basic S3 client that is used
|
163
|
+
# to make api calls. If a `:client` is not provided, a new {S3::Client}
|
164
|
+
# will be constructed.
|
165
|
+
#
|
166
|
+
# @option options [#key_for] :key_provider Any object that responds
|
167
|
+
# to `#key_for`. This method should accept a materials description
|
168
|
+
# JSON document string and return return an encryption key.
|
169
|
+
#
|
170
|
+
# @option options [OpenSSL::PKey::RSA, String] :encryption_key The master
|
171
|
+
# key to use for encrypting/decrypting all objects.
|
172
|
+
#
|
173
|
+
# @option options [Symbol] :envelope_location (:metadata) Where to
|
174
|
+
# store the envelope encryption keys. By default, the envelope is
|
175
|
+
# stored with the encrypted object. If you pass `:instruction_file`,
|
176
|
+
# then the envelope is stored in a seperate object in Amazon S3.
|
177
|
+
#
|
178
|
+
# @option options [String] :instruction_file_suffix ('.instruction')
|
179
|
+
# When `:envelope_location` is `:instruction_file` then the
|
180
|
+
# instruction file uses the object key with this suffix appended.
|
181
|
+
#
|
182
|
+
def initialize(options = {})
|
183
|
+
@client = options[:client] || S3::Client.new
|
184
|
+
@key_provider = extract_key_provider(options)
|
185
|
+
@envelope_location = extract_location(options)
|
186
|
+
@instruction_file_suffix = extract_suffix(options)
|
187
|
+
end
|
188
|
+
|
189
|
+
# @return [S3::Client]
|
190
|
+
attr_reader :client
|
191
|
+
|
192
|
+
# @return [KeyProvider]
|
193
|
+
attr_reader :key_provider
|
194
|
+
|
195
|
+
# @return [Symbol<:metadata, :instruction_file>]
|
196
|
+
attr_reader :envelope_location
|
197
|
+
|
198
|
+
# @return [String] When {#envelope_location} is `:instruction_file`,
|
199
|
+
# the envelope is stored in the object with the object key suffixed
|
200
|
+
# by this string.
|
201
|
+
attr_reader :instruction_file_suffix
|
202
|
+
|
203
|
+
# Calls {S3::Client#put_object}. The `:body` option will be encrypted
|
204
|
+
# client-side before sending data to Amazon S3.
|
205
|
+
# @see S3::Client#put_object
|
206
|
+
def put_object(options = {})
|
207
|
+
req = @client.build_request(:put_object, options)
|
208
|
+
req.handlers.add(EncryptHandler, priority: 95)
|
209
|
+
req.context[:encryption] = {
|
210
|
+
materials: @key_provider.encryption_materials,
|
211
|
+
envelope_location: @envelope_location,
|
212
|
+
instruction_file_suffix: @instruction_file_suffix,
|
213
|
+
}
|
214
|
+
req.send_request
|
215
|
+
end
|
216
|
+
|
217
|
+
# Performs an `#get_object` request on the `#config.client`. The
|
218
|
+
# response body is decrypted as it is read from the HTTP response
|
219
|
+
# socket. See {Client#get_object} for documentation on valid
|
220
|
+
# options.
|
221
|
+
def get_object(options = {})
|
222
|
+
if options[:range]
|
223
|
+
raise NotImplementedError, '#get_object with :range not supported yet'
|
224
|
+
end
|
225
|
+
|
226
|
+
location = options.delete(:envelope_location) || @envelope_location
|
227
|
+
suffix = options.delete(:instruction_file_suffix) || @instruction_file_suffix
|
228
|
+
|
229
|
+
req = @client.build_request(:get_object, options)
|
230
|
+
req.handlers.add(DecryptHandler)
|
231
|
+
req.context[:encryption] = {
|
232
|
+
key_provider: @key_provider,
|
233
|
+
envelope_location: location,
|
234
|
+
instruction_file_suffix: suffix,
|
235
|
+
}
|
236
|
+
req.send_request
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def extract_key_provider(options)
|
242
|
+
if options[:key_provider]
|
243
|
+
options[:key_provider]
|
244
|
+
elsif options[:encryption_key]
|
245
|
+
DefaultKeyProvider.new(options)
|
246
|
+
else
|
247
|
+
msg = "you must pass a :key_provider or :encryption_key"
|
248
|
+
raise ArgumentError, msg
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def extract_location(options)
|
253
|
+
location = options[:envelope_location] || :metadata
|
254
|
+
if [:metadata, :instruction_file].include?(location)
|
255
|
+
location
|
256
|
+
else
|
257
|
+
msg = ":envelope_location must be :metadata or :instruction_file "
|
258
|
+
msg << "got #{location.inspect}"
|
259
|
+
raise ArgumentError, msg
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def extract_suffix(options)
|
264
|
+
suffix = options[:instruction_file_suffix] || '.instruction'
|
265
|
+
if String === suffix
|
266
|
+
suffix
|
267
|
+
else
|
268
|
+
msg = ":instruction_file_suffix must be a String"
|
269
|
+
raise ArgumentError, msg
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Aws
|
2
|
+
module S3
|
3
|
+
module Encryption
|
4
|
+
class Config < Struct.new(
|
5
|
+
:client,
|
6
|
+
:key_provider,
|
7
|
+
:materials_description,
|
8
|
+
:materials_location,
|
9
|
+
:instruction_file_suffix)
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
DEFAULTS = {
|
13
|
+
client: lambda { S3::Client.new },
|
14
|
+
key_provider: lambda {
|
15
|
+
msg = "must specify an :encryption_key or :key_provider"
|
16
|
+
raise ArgumentError, msg
|
17
|
+
},
|
18
|
+
materials_description: '{}',
|
19
|
+
materials_location: :metadata,
|
20
|
+
instruction_file_suffix: '.instruction',
|
21
|
+
}
|
22
|
+
|
23
|
+
def encryption_key= master_key
|
24
|
+
self[:key_provider] = DefaultKeyProvider.new(master_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [Symbol] location :metadata Must be one of
|
28
|
+
# the following values:
|
29
|
+
# * :metadata
|
30
|
+
# * :instruction_file
|
31
|
+
def materials_location= location
|
32
|
+
unless [:metadata, :instruction_file].include?(location)
|
33
|
+
msg = ":materials_location must be :metadata or :instruction_file "
|
34
|
+
msg << "got #{location.inspect}"
|
35
|
+
raise ArgumentError, msg
|
36
|
+
end
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
|
42
|
+
def build(options)
|
43
|
+
config = Config.new
|
44
|
+
options.each do |opt_name, opt_value|
|
45
|
+
config.send("#{opt_name}=", opt_value)
|
46
|
+
end
|
47
|
+
apply_defaults(config)
|
48
|
+
config
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def apply_defaults(config)
|
54
|
+
DEFAULTS.each do |key, value|
|
55
|
+
if config[key].nil?
|
56
|
+
config[key] = value.is_a?(Proc) ? value.call : value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
module Encryption
|
6
|
+
# @api private
|
7
|
+
class DecryptHandler < Seahorse::Client::Handler
|
8
|
+
|
9
|
+
def call(context)
|
10
|
+
attach_http_event_listeners(context)
|
11
|
+
@handler.call(context)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def attach_http_event_listeners(context)
|
17
|
+
context.http_response.on_headers(200) do
|
18
|
+
cipher = decryption_cipher(context)
|
19
|
+
body = context.http_response.body
|
20
|
+
context.http_response.body = IODecrypter.new(cipher, body)
|
21
|
+
end
|
22
|
+
|
23
|
+
context.http_response.on_success(200) do
|
24
|
+
decrypter = context.http_response.body
|
25
|
+
decrypter.finalize
|
26
|
+
decrypter.io.rewind if decrypter.io.respond_to?(:rewind)
|
27
|
+
context.http_response.body = decrypter.io
|
28
|
+
end
|
29
|
+
|
30
|
+
context.http_response.on_error do
|
31
|
+
if context.http_response.body.is_a?(IODecrypter)
|
32
|
+
context.http_response.body = context.http_response.body.io
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def decryption_cipher(context)
|
38
|
+
if envelope = get_encryption_envelope(context)
|
39
|
+
key_provider = context[:encryption][:key_provider]
|
40
|
+
key = key_provider.key_for(envelope['x-amz-matdesc'])
|
41
|
+
envelope_key = Utils.decrypt(key, decode64(envelope['x-amz-key']))
|
42
|
+
envelope_iv = decode64(envelope['x-amz-iv'])
|
43
|
+
Utils.aes_decryption_cipher(:CBC, envelope_key, envelope_iv)
|
44
|
+
else
|
45
|
+
raise Errors::DecryptionError, "unable to locate encyrption envelope"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_encryption_envelope(context)
|
50
|
+
if context[:encryption][:envelope_location] == :metadata
|
51
|
+
envelope_from_metadata(context) || envelope_from_instr_file(context)
|
52
|
+
else
|
53
|
+
envelope_from_instr_file(context) || envelope_from_metadata(context)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def envelope_from_metadata(context)
|
58
|
+
keys = %w(x-amz-key x-amz-iv x-amz-matdesc)
|
59
|
+
envelope = keys.each.with_object({}) do |key, hash|
|
60
|
+
if value = context.http_response.headers["x-amz-meta-#{key}"]
|
61
|
+
hash[key] = value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
envelope.keys == keys ? envelope : nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def envelope_from_instr_file(context)
|
68
|
+
suffix = context[:encryption][:instruction_file_suffix]
|
69
|
+
MultiJson.load(context.client.get_object(
|
70
|
+
bucket: context.params[:bucket],
|
71
|
+
key: context.params[:key] + suffix
|
72
|
+
).body.read)
|
73
|
+
rescue S3::Errors::ServiceError, MultiJson::ParseError
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def decode64(str)
|
78
|
+
Base64.decode64(str)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Aws
|
2
|
+
module S3
|
3
|
+
module Encryption
|
4
|
+
|
5
|
+
# The default key provider is constructed with a single key
|
6
|
+
# that is used for both encryption and decryption, ignoring
|
7
|
+
# the possible per-object envelope encryption materials description.
|
8
|
+
# @api private
|
9
|
+
class DefaultKeyProvider
|
10
|
+
|
11
|
+
include KeyProvider
|
12
|
+
|
13
|
+
# @option options [required, OpenSSL::PKey::RSA, String] :encryption_key
|
14
|
+
# The master key to use for encrypting objects.
|
15
|
+
# @option options [String<JSON>] :materials_description ('{}')
|
16
|
+
# A description of the encyrption key.
|
17
|
+
def initialize(options = {})
|
18
|
+
@encryption_materials = Materials.new(
|
19
|
+
key: options[:encryption_key],
|
20
|
+
description: options[:materials_description] || '{}'
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Materials]
|
25
|
+
def encryption_materials
|
26
|
+
@encryption_materials
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param [String<JSON>] materials_description
|
30
|
+
# @return Returns the key given in the constructor.
|
31
|
+
def key_for(materials_description)
|
32
|
+
@encryption_materials.key
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
module Encryption
|
6
|
+
# @api private
|
7
|
+
class EncryptHandler < Seahorse::Client::Handler
|
8
|
+
|
9
|
+
def call(context)
|
10
|
+
cipher = Utils.aes_encryption_cipher(:CBC)
|
11
|
+
context[:encryption][:cipher] = cipher
|
12
|
+
envelope = {
|
13
|
+
'x-amz-key' => encode64(encrypt(context, envelope_key(cipher))),
|
14
|
+
'x-amz-iv' => encode64(envelope_iv(cipher)),
|
15
|
+
'x-amz-matdesc' => context[:encryption][:materials].description,
|
16
|
+
}
|
17
|
+
apply_encryption_envelope(context, envelope)
|
18
|
+
apply_encryption_cipher(context, cipher)
|
19
|
+
@handler.call(context)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def apply_encryption_envelope(context, envelope)
|
25
|
+
if context[:encryption][:envelope_location] == :metadata
|
26
|
+
context.params[:metadata] ||= {}
|
27
|
+
context.params[:metadata].update(envelope)
|
28
|
+
else # :instruction_file
|
29
|
+
suffix = context[:encryption][:instruction_file_suffix]
|
30
|
+
context.client.put_object(
|
31
|
+
bucket: context.params[:bucket],
|
32
|
+
key: context.params[:key] + suffix,
|
33
|
+
body: MultiJson.dump(envelope)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def apply_encryption_cipher(context, cipher)
|
39
|
+
io = context.params[:body] || ''
|
40
|
+
io = StringIO.new(io) if String === io
|
41
|
+
context.params[:body] = IOEncrypter.new(cipher, io)
|
42
|
+
context.params[:metadata] ||= {}
|
43
|
+
context.params[:metadata]['x-amz-unencrypted-content-length'] = io.size
|
44
|
+
if md5 = context.params.delete(:content_md5)
|
45
|
+
context.params[:metadata]['x-amz-unencrypted-content-md5'] = md5
|
46
|
+
end
|
47
|
+
context.http_response.on_headers do
|
48
|
+
context.params[:body].close
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def envelope_key(cipher)
|
53
|
+
cipher.key = cipher.random_key
|
54
|
+
end
|
55
|
+
|
56
|
+
def envelope_iv(cipher)
|
57
|
+
cipher.iv = cipher.random_iv
|
58
|
+
end
|
59
|
+
|
60
|
+
def encode64(str)
|
61
|
+
Base64.encode64(str).split("\n") * ""
|
62
|
+
end
|
63
|
+
|
64
|
+
def encrypt(context, data)
|
65
|
+
Utils.encrypt(context[:encryption][:materials].key, data)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Aws
|
2
|
+
module S3
|
3
|
+
module Encryption
|
4
|
+
# @api private
|
5
|
+
class IODecrypter
|
6
|
+
|
7
|
+
# @param [OpenSSL::Cipher] cipher
|
8
|
+
# @param [#write] io An IO-like object that responds to {#write}.
|
9
|
+
def initialize(cipher, io)
|
10
|
+
@orig_cipher = cipher.clone
|
11
|
+
@cipher = cipher.clone
|
12
|
+
@io = io
|
13
|
+
reset_cipher
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [#write]
|
17
|
+
attr_reader :io
|
18
|
+
|
19
|
+
def write(chunk)
|
20
|
+
@io.write(@cipher.update(chunk))
|
21
|
+
end
|
22
|
+
|
23
|
+
def finalize
|
24
|
+
@io.write(@cipher.final)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def reset_cipher
|
30
|
+
@cipher = @orig_cipher.clone
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Aws
|
5
|
+
module S3
|
6
|
+
module Encryption
|
7
|
+
|
8
|
+
# Provides an IO wrapper encrpyting a stream of data.
|
9
|
+
# It is possible to use this same object for decrypting. You must
|
10
|
+
# initialize it with a decryptiion cipher in that case and the
|
11
|
+
# IO object must contain cipher text instead of plain text.
|
12
|
+
# @api private
|
13
|
+
class IOEncrypter
|
14
|
+
|
15
|
+
# @api private
|
16
|
+
ONE_MEGABYTE = 1024 * 1024
|
17
|
+
|
18
|
+
def initialize(cipher, io)
|
19
|
+
@encrypted = io.size <= ONE_MEGABYTE ?
|
20
|
+
encrypt_to_stringio(cipher, io.read) :
|
21
|
+
encrypt_to_tempfile(cipher, io)
|
22
|
+
@size = @encrypted.size
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Integer]
|
26
|
+
attr_reader :size
|
27
|
+
|
28
|
+
def read(bytes = nil, output_buffer = nil)
|
29
|
+
if Tempfile === @encrypted && @encrypted.closed?
|
30
|
+
@encrypted.open
|
31
|
+
@encrypted.binmode
|
32
|
+
end
|
33
|
+
@encrypted.read(bytes, output_buffer)
|
34
|
+
end
|
35
|
+
|
36
|
+
def rewind
|
37
|
+
@encrypted.rewind
|
38
|
+
end
|
39
|
+
|
40
|
+
# @api private
|
41
|
+
def close
|
42
|
+
@encrypted.close if Tempfile === @encrypted
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def encrypt_to_stringio(cipher, plain_text)
|
48
|
+
if plain_text.empty?
|
49
|
+
StringIO.new(cipher.final)
|
50
|
+
else
|
51
|
+
StringIO.new(cipher.update(plain_text) + cipher.final)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def encrypt_to_tempfile(cipher, io)
|
56
|
+
encrypted = Tempfile.new(self.object_id.to_s)
|
57
|
+
encrypted.binmode
|
58
|
+
while chunk = io.read(ONE_MEGABYTE)
|
59
|
+
encrypted.write(cipher.update(chunk))
|
60
|
+
end
|
61
|
+
encrypted.write(cipher.final)
|
62
|
+
encrypted.rewind
|
63
|
+
encrypted
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Aws
|
2
|
+
module S3
|
3
|
+
module Encryption
|
4
|
+
|
5
|
+
# This module defines the interface required for a {Client#key_provider}.
|
6
|
+
# A key provider is any object that:
|
7
|
+
#
|
8
|
+
# * Responds to {#encryption_materials} with an {Materials} object.
|
9
|
+
#
|
10
|
+
# * Responds to {#key_for}, receiving a JSON document String,
|
11
|
+
# returning an ecryption key. The returned encryption key
|
12
|
+
# must be one of:
|
13
|
+
#
|
14
|
+
# * `OpenSSL::PKey::RSA` - for asymmetric encryption
|
15
|
+
# * `String` - 32, 24, or 16 bytes long, for symmetric encryption
|
16
|
+
#
|
17
|
+
module KeyProvider
|
18
|
+
|
19
|
+
# @return [Materials]
|
20
|
+
def encryption_materials; end
|
21
|
+
|
22
|
+
# @param [String<JSON>] materials_description
|
23
|
+
# @return [OpenSSL::PKey::RSA, String] encryption_key
|
24
|
+
def key_for(materials_description); end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
module Encryption
|
6
|
+
class Materials
|
7
|
+
|
8
|
+
# @option options [required, OpenSSL::PKey::RSA, String] :encryption_key
|
9
|
+
# The master key to use for encrypting/decrypting all objects.
|
10
|
+
#
|
11
|
+
# @option options [String<JSON>] :materials_description ('{}')
|
12
|
+
# The encryption materials description. This is must be
|
13
|
+
# a JSON document string.
|
14
|
+
#
|
15
|
+
# @option options [Symbol] :materials_location (:metadata) Where to
|
16
|
+
# store the envelope encryption keys. This must be one of
|
17
|
+
# the following values:
|
18
|
+
#
|
19
|
+
# * `:metadata`
|
20
|
+
# * `:instruction_file`
|
21
|
+
#
|
22
|
+
# @option options [String] :instruction_file_suffix ('.instruction')
|
23
|
+
#
|
24
|
+
def initialize(options = {})
|
25
|
+
@key = validate_key(options[:key])
|
26
|
+
@description = validate_desc(options[:description])
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [OpenSSL::PKey::RSA, String]
|
30
|
+
attr_reader :key
|
31
|
+
|
32
|
+
# @return [String<JSON>]
|
33
|
+
attr_reader :description
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def validate_key(key)
|
38
|
+
case key
|
39
|
+
when OpenSSL::PKey::RSA then key
|
40
|
+
when String
|
41
|
+
if [32, 24, 16].include?(key.bytesize)
|
42
|
+
key
|
43
|
+
else
|
44
|
+
msg = "invalid key, symmetric key required to be 16, 24, or "
|
45
|
+
msg << "32 bytes in length, saw length 31"
|
46
|
+
raise ArgumentError, msg
|
47
|
+
end
|
48
|
+
else
|
49
|
+
msg = "invalid encryption key, expected an OpenSSL::PKey::RSA key "
|
50
|
+
msg << "(for asymmetric encryption) or a String (for symmetric "
|
51
|
+
msg << "encryption)."
|
52
|
+
raise ArgumentError, msg
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_desc(description)
|
57
|
+
MultiJson.load(description)
|
58
|
+
description
|
59
|
+
rescue MultiJson::ParseError
|
60
|
+
msg = "expected description to be a valid JSON document string"
|
61
|
+
raise ArgumentError, msg
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
module Encryption
|
6
|
+
# @api private
|
7
|
+
module Utils
|
8
|
+
|
9
|
+
UNSAFE_MSG = "unsafe encryption, data is longer than key length"
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
def encrypt(key, data)
|
14
|
+
case key
|
15
|
+
when OpenSSL::PKey::RSA # asymmetric encryption
|
16
|
+
warn(UNSAFE_MSG) if key.public_key.n.num_bits < cipher_size(data)
|
17
|
+
key.public_encrypt(data)
|
18
|
+
when String # symmetric encryption
|
19
|
+
warn(UNSAFE_MSG) if cipher_size(key) < cipher_size(data)
|
20
|
+
cipher = aes_encryption_cipher(:ECB, key)
|
21
|
+
cipher.update(data) + cipher.final
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def decrypt(key, data)
|
26
|
+
rsa = OpenSSL::PKey::RSA
|
27
|
+
begin
|
28
|
+
case key
|
29
|
+
when OpenSSL::PKey::RSA # asymmetric decryption
|
30
|
+
key.private_decrypt(data)
|
31
|
+
when String # symmetric Decryption
|
32
|
+
cipher = aes_cipher(:decrypt, :ECB, key, nil)
|
33
|
+
cipher.update(data) + cipher.final
|
34
|
+
end
|
35
|
+
rescue OpenSSL::Cipher::CipherError
|
36
|
+
msg = 'decryption failed, possible incorrect key'
|
37
|
+
raise Errors::DecryptionError, msg
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param [String] block_mode "CBC" or "ECB"
|
42
|
+
# @param [OpenSSL::PKey::RSA, String, nil] key
|
43
|
+
# @param [String, nil] iv The initialization vector
|
44
|
+
def aes_encryption_cipher(block_mode, key = nil, iv = nil)
|
45
|
+
aes_cipher(:encrypt, block_mode, key, iv)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param [String] block_mode "CBC" or "ECB"
|
49
|
+
# @param [OpenSSL::PKey::RSA, String, nil] key
|
50
|
+
# @param [String, nil] iv The initialization vector
|
51
|
+
def aes_decryption_cipher(block_mode, key = nil, iv = nil)
|
52
|
+
aes_cipher(:decrypt, block_mode, key, iv)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [String] mode "encrypt" or "decrypt"
|
56
|
+
# @param [String] block_mode "CBC" or "ECB"
|
57
|
+
# @param [OpenSSL::PKey::RSA, String, nil] key
|
58
|
+
# @param [String, nil] iv The initialization vector
|
59
|
+
def aes_cipher(mode, block_mode, key, iv)
|
60
|
+
cipher = key ?
|
61
|
+
OpenSSL::Cipher.new("AES-#{cipher_size(key)}-#{block_mode}") :
|
62
|
+
OpenSSL::Cipher.new("AES-256-#{block_mode}")
|
63
|
+
cipher.send(mode) # encrypt or decrypt
|
64
|
+
cipher.key = key if key
|
65
|
+
cipher.iv = iv if iv
|
66
|
+
cipher
|
67
|
+
end
|
68
|
+
|
69
|
+
# @param [String] key
|
70
|
+
# @return [Integer]
|
71
|
+
# @raise ArgumentError
|
72
|
+
def cipher_size(key)
|
73
|
+
key.bytesize * 8
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aws-sdk-resources
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.15.pre
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Amazon Web Services
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-12-
|
11
|
+
date: 2014-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-core
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 2.0.
|
19
|
+
version: 2.0.15
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 2.0.
|
26
|
+
version: 2.0.15
|
27
27
|
description: Provides resource-oriented abstractions for AWS.
|
28
28
|
email:
|
29
29
|
executables: []
|
@@ -53,6 +53,18 @@ files:
|
|
53
53
|
- lib/aws-sdk-resources/services/ec2/instance.rb
|
54
54
|
- lib/aws-sdk-resources/services/ec2.rb
|
55
55
|
- lib/aws-sdk-resources/services/s3/bucket.rb
|
56
|
+
- lib/aws-sdk-resources/services/s3/encryption/client.rb
|
57
|
+
- lib/aws-sdk-resources/services/s3/encryption/config.rb
|
58
|
+
- lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb
|
59
|
+
- lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb
|
60
|
+
- lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb
|
61
|
+
- lib/aws-sdk-resources/services/s3/encryption/errors.rb
|
62
|
+
- lib/aws-sdk-resources/services/s3/encryption/io_decrypter.rb
|
63
|
+
- lib/aws-sdk-resources/services/s3/encryption/io_encrypter.rb
|
64
|
+
- lib/aws-sdk-resources/services/s3/encryption/key_provider.rb
|
65
|
+
- lib/aws-sdk-resources/services/s3/encryption/materials.rb
|
66
|
+
- lib/aws-sdk-resources/services/s3/encryption/utils.rb
|
67
|
+
- lib/aws-sdk-resources/services/s3/encryption.rb
|
56
68
|
- lib/aws-sdk-resources/services/s3/file_part.rb
|
57
69
|
- lib/aws-sdk-resources/services/s3/file_uploader.rb
|
58
70
|
- lib/aws-sdk-resources/services/s3/multipart_file_uploader.rb
|