wt_s3_signer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/wt_s3_signer.rb +183 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 99d14080be11be7b5aeed75a44dba5001d2bcff106c5277ec679666381efe5c5
|
4
|
+
data.tar.gz: dfa138218f64787226846defd64051c76ad65d95b81d420919357a7366cfcda3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cea9671eff80aa7ae3919f6e3a684238191329cb694857eb33c1478603078c77f62a8d1c62357f804014f4c69329814fe35a0eca9a84f5101b7e7f99e7ba6ebd
|
7
|
+
data.tar.gz: df2fea55f7c659d08cf8184da7d9b921e34d575e4b0731a6f4a50ac6230c64c7a501123f1711e8a1ba87877da42527aaeb9636a00927265222ccc76dba12b6cc
|
data/lib/wt_s3_signer.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'digest'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
# An accelerated version of the reference implementation ported
|
6
|
+
# from Python, see here:
|
7
|
+
#
|
8
|
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
|
9
|
+
#
|
10
|
+
# The optimisation in comparison to the ref implementation
|
11
|
+
# is that everything that can be computed once gets computed for the
|
12
|
+
# first signature being generated, and then reused. This includes
|
13
|
+
# the timestamp and everything derived from it, the signing key
|
14
|
+
# and the query string (before the signature is computed).
|
15
|
+
#
|
16
|
+
# Note that this is specifically made for the cases where one needs
|
17
|
+
# presigned URLs for multiple objects from the same bucket, with the same
|
18
|
+
# expiry. Passing the expiry via the constructor, for instance, allows us
|
19
|
+
# to cache more of the query string - saving even more time.
|
20
|
+
module WT
|
21
|
+
class S3Signer
|
22
|
+
|
23
|
+
# Creates a new instance of WT::S3Signer for a given S3 bucket object.
|
24
|
+
# This object can be created in the AWS SDK using `Aws::S3::Bucket.new(my_bucket_name)`.
|
25
|
+
# The bucket object helps resolving the bucket endpoint URL, determining the bucket
|
26
|
+
# region and so forth.
|
27
|
+
#
|
28
|
+
# @param bucket[Aws::S3::Bucket] the AWS bucket resource object
|
29
|
+
# @param extra_attributes[Hash] any extra keyword arguments to pass to `S3Signer.new`
|
30
|
+
# @return [WT::S3Signer]
|
31
|
+
def self.for_s3_bucket(bucket, **extra_attributes)
|
32
|
+
kwargs = {}
|
33
|
+
|
34
|
+
kwargs[:bucket_endpoint_url] = bucket.url
|
35
|
+
kwargs[:bucket_host] = URI.parse(bucket.url).host
|
36
|
+
kwargs[:bucket_name] = bucket.name
|
37
|
+
|
38
|
+
client = Aws::S3::Client.new
|
39
|
+
resp = client.get_bucket_location(bucket: bucket.name)
|
40
|
+
aws_region = resp.data.location_constraint
|
41
|
+
|
42
|
+
# us-east-1 is a special AWS region (the oldest) and one
|
43
|
+
# of the specialties is that when you ask for the region
|
44
|
+
# of a bucket you get an empty string back instead of the
|
45
|
+
# actual name of the region. We need to compensate for that
|
46
|
+
# because if our region name is empty our signature will _not_
|
47
|
+
# be accepted by S3 (but only for buckets in the us-east-1 region!)
|
48
|
+
kwargs[:aws_region] = aws_region == "" ? "us-east-1" : aws_region
|
49
|
+
|
50
|
+
credentials = client.config.credentials
|
51
|
+
credentials = credentials.credentials if credentials.respond_to?(:credentials)
|
52
|
+
kwargs[:access_key_id] = credentials.access_key_id
|
53
|
+
kwargs[:secret_access_key] = credentials.secret_access_key
|
54
|
+
kwargs[:session_token] = credentials.session_token
|
55
|
+
|
56
|
+
new(**kwargs, **extra_attributes)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Creates a new instance of WT::S3Signer
|
60
|
+
#
|
61
|
+
# @param now[Time] The timestamp to use for the signature (the `expires_in` is also relative to that time)
|
62
|
+
# @param expires_in[Integer] The number of seconds the URL will stay current from `now`
|
63
|
+
# @param aws_region[String] The name of the AWS region. Also needs to be set to "us-east-1" for the respective region.
|
64
|
+
# @param bucket_endpoint_url[String] The endpoint URL for the bucket (usually same as the bucket hostname as resolved by the SDK)
|
65
|
+
# @param bucket_host[String] The bucket endpoint hostname (usually derived from the bucket endpoint URL)
|
66
|
+
# @param bucket_name[String] The bucket name
|
67
|
+
# @param access_key_id[String] The IAM access key ID
|
68
|
+
# @param secret_access_key[String] The IAM secret access key
|
69
|
+
# @param session_token[String,nil] The IAM session token if STS sessions are used
|
70
|
+
def initialize(now: Time.now, expires_in:, aws_region:, bucket_endpoint_url:, bucket_host:, bucket_name:, access_key_id:, secret_access_key:, session_token:)
|
71
|
+
@region = aws_region
|
72
|
+
@service = "s3"
|
73
|
+
|
74
|
+
@expires_in = expires_in
|
75
|
+
@bucket_endpoint = bucket_endpoint_url
|
76
|
+
@bucket_host = bucket_host
|
77
|
+
@bucket_name = bucket_name
|
78
|
+
@now = now.utc
|
79
|
+
@secret_key = secret_access_key
|
80
|
+
@access_key = access_key_id
|
81
|
+
@session_token = session_token
|
82
|
+
end
|
83
|
+
|
84
|
+
# Creates a signed URL for the given S3 object key.
|
85
|
+
# The URL is temporary and the expiration time is based on the
|
86
|
+
# expires_in value on initialize
|
87
|
+
#
|
88
|
+
# @param object_key[String] The S3 key that needs a presigned url
|
89
|
+
#
|
90
|
+
# @raise [ArgumentError] Raises an ArgumentError if `object_key:`
|
91
|
+
# is empty.
|
92
|
+
#
|
93
|
+
# @return [String] The signed url
|
94
|
+
def presigned_get_url(object_key:)
|
95
|
+
# Variables that do not change during consecutive calls to the
|
96
|
+
# method are instance variables. This way they are not assigned
|
97
|
+
# every single time and are cached
|
98
|
+
if (object_key.nil? || object_key == "")
|
99
|
+
raise ArgumentError, "object_key: must not be empty"
|
100
|
+
end
|
101
|
+
|
102
|
+
@datestamp ||= @now.strftime("%Y%m%d")
|
103
|
+
@amz_date ||= @now.strftime("%Y%m%dT%H%M%SZ")
|
104
|
+
|
105
|
+
# ------ TASK 1: Create the canonical request
|
106
|
+
# -- Step 1: define the method
|
107
|
+
@method ||= "GET"
|
108
|
+
|
109
|
+
# -- Step 2: create canonical uri
|
110
|
+
# The canonical URI (the URI path) is the only thing
|
111
|
+
# that changes depending on the object key
|
112
|
+
canonical_uri = "/" + object_key # Might need URL escaping (!)
|
113
|
+
|
114
|
+
# -- Step 3: create the canonical headers
|
115
|
+
@canonical_headers ||= "host:" + @bucket_host + "\n"
|
116
|
+
@signed_headers ||= "host"
|
117
|
+
|
118
|
+
# -- Step 4: create the canonical query string
|
119
|
+
@algorithm ||= "AWS4-HMAC-SHA256"
|
120
|
+
@credential_scope ||= @datestamp + "/" + @region + "/" + @service + "/" + "aws4_request"
|
121
|
+
|
122
|
+
@canonical_querystring_template ||= begin
|
123
|
+
[
|
124
|
+
"X-Amz-Algorithm=#{@algorithm}",
|
125
|
+
"X-Amz-Credential=" + CGI.escape(@access_key + "/" + @credential_scope),
|
126
|
+
"X-Amz-Date=" + @amz_date,
|
127
|
+
"X-Amz-Expires=%d" % @expires_in,
|
128
|
+
# ------- When using STS we also need to add the security token
|
129
|
+
("X-Amz-Security-Token=" + CGI.escape(@session_token) if @session_token),
|
130
|
+
"X-Amz-SignedHeaders=" + @signed_headers,
|
131
|
+
].compact.join('&')
|
132
|
+
end
|
133
|
+
|
134
|
+
# -- Step 5: create payload
|
135
|
+
@payload ||= "UNSIGNED-PAYLOAD"
|
136
|
+
|
137
|
+
# -- Step 6: combine elements to create the canonical request
|
138
|
+
canonical_request = [
|
139
|
+
@method,
|
140
|
+
canonical_uri,
|
141
|
+
@canonical_querystring_template,
|
142
|
+
@canonical_headers,
|
143
|
+
@signed_headers,
|
144
|
+
@payload
|
145
|
+
].join("\n")
|
146
|
+
|
147
|
+
# ------ TASK 2: Create a String to sign
|
148
|
+
string_to_sign = [
|
149
|
+
@algorithm,
|
150
|
+
@amz_date,
|
151
|
+
@credential_scope,
|
152
|
+
Digest::SHA256.hexdigest(canonical_request)
|
153
|
+
].join("\n")
|
154
|
+
|
155
|
+
# ------ TASK 3: Calculate the signature
|
156
|
+
@signing_key ||= derive_signing_key(@secret_key, @datestamp, @region, @service)
|
157
|
+
signature = OpenSSL::HMAC.hexdigest("SHA256", @signing_key, string_to_sign)
|
158
|
+
|
159
|
+
# ------ TASK 4: Add signing information to the request
|
160
|
+
qs_with_signature = @canonical_querystring_template + "&X-Amz-Signature=" + signature
|
161
|
+
|
162
|
+
@bucket_endpoint + canonical_uri + "?" + qs_with_signature
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def create_bucket(bucket_name)
|
168
|
+
Aws::S3::Bucket.new(bucket_name)
|
169
|
+
end
|
170
|
+
|
171
|
+
def derive_signing_key(key, datestamp, region, service)
|
172
|
+
prefixed_key = "AWS4" + key
|
173
|
+
k_date = hmac_bytes(prefixed_key, datestamp)
|
174
|
+
k_region = hmac_bytes(k_date, region)
|
175
|
+
k_service = hmac_bytes(k_region, service)
|
176
|
+
hmac_bytes(k_service, "aws4_request")
|
177
|
+
end
|
178
|
+
|
179
|
+
def hmac_bytes(key, data)
|
180
|
+
OpenSSL::HMAC.digest("SHA256", key, data)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wt_s3_signer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Luca Suriano
|
8
|
+
- Julik Tarkhanov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2019-12-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: aws-sdk-s3
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: yard
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.9.24
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.9.24
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rake
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 13.0.1
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 13.0.1
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.9'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.9'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rspec-benchmark
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 0.5.1
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 0.5.1
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rubocop
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
description: A Ruby Gem that optimize the signing of S3 keys. The gem is especially
|
99
|
+
useful when dealing with a large amount of S3 object keys
|
100
|
+
email:
|
101
|
+
- luca.suriano@wetransfer.com
|
102
|
+
- me@julik.nl
|
103
|
+
executables: []
|
104
|
+
extensions: []
|
105
|
+
extra_rdoc_files: []
|
106
|
+
files:
|
107
|
+
- lib/wt_s3_signer.rb
|
108
|
+
homepage: https://github.com/WeTransfer/wt_s3_signer
|
109
|
+
licenses:
|
110
|
+
- MIT (Hippocratic)
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubygems_version: 3.0.3
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: A library for signing S3 key faster
|
131
|
+
test_files: []
|