wt_s3_signer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/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: []
|