wt_s3_signer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/wt_s3_signer.rb +183 -0
  3. metadata +131 -0
@@ -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
@@ -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: []