right_support 2.14.0 → 2.14.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1e82daed673214599b4e0206f8a766ba400d06ac
4
- data.tar.gz: e6d9f64f58a4b3932e725dbbdaf071a094c4b4de
3
+ metadata.gz: 1dca63bf8c124604e44a21a45d56515e63fc6760
4
+ data.tar.gz: 224209a39c20b16906f9688755ea12b16957655e
5
5
  SHA512:
6
- metadata.gz: 76d3073f77651f586ae49aa3f956498512945584abe938baaa0e3ce8e0c68c94ed90ede39316aae6551c33f49957c3b06a0cd0a545b8f35ed8c29e18d1dc2511
7
- data.tar.gz: b72d3a15b6cea55871a7a1f8a0ff7709aed8bba348bae85bb279d66b937be26f6edea2b49ae5a426f8d19aa161d7b9699803a82c8ab106b48c6ab546c0764142
6
+ metadata.gz: ab60e79b75e481d2c374fecf4f59f5c69bade7f859b3ad34d025b397cfb431bce2c6ef769f50406723b9170c2391c089e94255a7386760884a52b41ede8c9684
7
+ data.tar.gz: 7fc92ba8a0e765a51c043bc39ab11fcf664d2c2fad5330b18dcb42c4257a55b91f1726889b941c06a7d83c1f63c220e853903a6b74c7aefde10fecca00406a6c
@@ -24,37 +24,18 @@ require 'digest/md5'
24
24
  require 'digest/sha1'
25
25
  require 'digest/sha2'
26
26
  require 'openssl'
27
+ require 'base64'
27
28
 
28
29
  module RightSupport::Crypto
29
30
  # An easy way to compute digital signatures of data contained in a Ruby hash. To work with
30
31
  # signed hashes, you must first obtain an asymmetric key pair (any subclass of OpenSSL::PKey);
31
32
  # you can generate it from scratch or load it from a file on disk.
32
33
  #
33
- # Signature computation is influenced by four factors:
34
- # - The digital signature algorithm and key length
35
- # - The encoding used to serialize the hash contents to a byte stream
36
- # - The hash algorithm used to compute a message digest of the byte stream
37
- # - The OpenSSL API level used (EVP or raw crypto API)
34
+ # This class supports RSA, DSA and ECDSA signature methods. When used with
35
+ # ECDSA, it _only_ supports JWT envelope format. The intent is to enable
36
+ # consumers of this class to switch to JSON Web Token in the future.
38
37
  #
39
- # You are responsible for providing the PKey object, which determines the signature algorithm
40
- # and key length. This occasionally constrains your choice of hash algorithm; for instance,
41
- # a 512-bit RSA key would not be sufficiently long to create signatures of a SHA3-512 hash
42
- # due to the mathematical underpinnings of the RSA cipher. In practice this is not an issue,
43
- # because you should be using strong RSA keys (2048 bit or higher) for security reasons,
44
- # and even the strongest hash algorithms do not exceed 512-bit output.
45
- #
46
- # SignedHash provides reasonable defaults for the other three factors:
47
- # - JSON for message encoding (Yajl gem, JSON gem, Oj gem or built-in Ruby 1.9 JSON)
48
- # - SHA1 for message digest
49
- # - raw crypto API (for compatibility with older RightSupport versions)
50
- #
51
- # If you are adopting SignedHash for a new use case, it's best to use the default
52
- # encoding and message digest, but specify :envelope=>true to use the OpenSSL EVP
53
- # API! Using an envelope provides better protection against various cryptographic
54
- # attacks and ensures that the sign and verify operations can't be used.
55
- #
56
- # SignedHash defaults to raw-crypto signatures for compatibility reasons, but
57
- # with RightSupport v3 the raw-crypto will be deprecated and EVP will be used by default.
38
+ # @deprecated please use JSON Web Token instead of this class!
58
39
  #
59
40
  # @see OpenSSL::PKey
60
41
  # @see Digest
@@ -77,40 +58,77 @@ module RightSupport::Crypto
77
58
  DefaultEncoding = nil
78
59
  end unless defined?(DefaultEncoding)
79
60
 
80
- DEFAULT_OPTIONS = {
81
- :digest => Digest::SHA1,
82
- :envelope => false,
83
- :encoding => DefaultEncoding,
84
- }
85
-
86
- # Mapping of Ruby built-in hash algorithms to their OpenSSL counterparts
61
+ # List of acceptable Hash algorihtms + map of Ruby builtins to OpenSSL
62
+ # counterparts.
87
63
  DIGEST_MAP = {
88
64
  Digest::MD5 => OpenSSL::Digest::MD5,
89
65
  Digest::SHA1 => OpenSSL::Digest::SHA1,
90
66
  Digest::SHA2 => OpenSSL::Digest::SHA256,
91
- }
67
+ OpenSSL::Digest::MD5 => OpenSSL::Digest::MD5,
68
+ OpenSSL::Digest::SHA1 => OpenSSL::Digest::SHA1,
69
+ OpenSSL::Digest::SHA256 => OpenSSL::Digest::SHA256,
70
+ OpenSSL::Digest::SHA384 => OpenSSL::Digest::SHA384,
71
+ OpenSSL::Digest::SHA512 => OpenSSL::Digest::SHA512,
72
+ }.freeze
73
+
74
+ # Digest output sizes (in bits)
75
+ HASHSIZE_MAP = {
76
+ Digest::MD5 => 128,
77
+ Digest::SHA1 => 160,
78
+ Digest::SHA2 => 256,
79
+ OpenSSL::Digest::MD5 => 128,
80
+ OpenSSL::Digest::SHA1 => 160,
81
+ OpenSSL::Digest::SHA256 => 256,
82
+ OpenSSL::Digest::SHA384 => 384,
83
+ OpenSSL::Digest::SHA512 => 512,
84
+ }.freeze
92
85
 
93
86
  # Create a new sign/verify context, passing in a Hash full of data that is to be signed or
94
87
  # verified. The new SignedHash will store a reference to the raw data, so be careful not to
95
88
  # modify the data hash in a way that will influence the outcome of sign/verify!
96
89
  #
97
- # @param [Hash] hash the actual data that is to be signed
98
- # @option opts [Class] :digest hash-algorithm class from Ruby's Digest module MD5, SHA1 or SHA2; default SHA1
99
- # @option opts [true,false] :envelope use the OpenSSL EVP API if true, or raw-crypto API if false; default false
100
- # @option opts [#dump] :encoding serialization method for dumping hash data; default DefaultEncoding
101
- # @option opts [OpenSSL::PKey] :public_key key to use when verifying digital signatures
102
- # @option opts [OpenSSL::PKey] :private_key key to use when computing digital signatures
90
+ # @param [Hash] data the actual data that is to be signed
91
+ # @param [OpenSSL::PKey::PKey] key
92
+ # @param [Digest::Base, OpenSSL::Digest] digest hash function to use
93
+ # @param [#dump] encoding
94
+ # @param [nil,:none,:right_support,:jwt] envelope serialization format and encryption scheme; default depends on key type
95
+ # @param [OpenSSL::PKey::PKey] private_key deprecated -- pass key instead
96
+ # @param [OpenSSL::PKey::PKey] public_key deprecated -- pass key instead
97
+ def initialize(data={}, key=nil, digest:nil, encoding:DefaultEncoding, envelope:nil, private_key:nil, public_key:nil)
98
+ # Cope with legacy parameter
99
+ if key.nil?
100
+ key = private_key || public_key
101
+ warn(':private_key and :public_key are deprecated; please pass key as 2nd parameter') if key
102
+ end
103
+
104
+ @data = data
105
+ @encoding = encoding
106
+ @key = key
107
+
108
+ # Figure out envelope type
109
+ env = case envelope
110
+ when nil then guess_envelope
111
+ when :none, false then :none
112
+ when :right_support, true then :right_support
113
+ when :jwt then :jwt
114
+ else raise ArgumentError.new("Unsupported envelope #{envelope.inspect}")
115
+ end
116
+ @envelope = env
117
+
118
+ # Figure out digest algorithm
119
+ @digest = digest || guess_digest
120
+
121
+ check_parameters
122
+ end
123
+
124
+ # Compute a signature and return a JSON Web Token representation of
125
+ # this hash.
103
126
  #
104
- # @see DefaultEncoding
105
- def initialize(hash={}, opts={})
106
- opts = DEFAULT_OPTIONS.merge(opts)
107
- @hash = hash
108
- @digest = opts[:digest]
109
- @encoding = opts[:encoding]
110
- @envelope = !!opts[:envelope]
111
- @public_key = opts[:public_key]
112
- @private_key = opts[:private_key]
113
- duck_type_check
127
+ # @return [String] a signed JWT with the specified expiration timestamp
128
+ def to_jwt(expires_at)
129
+ prefix, sig = sign_with_canonical_representation(expires_at)
130
+ sig = RightSupport::Data::Base64URL.encode(sig)
131
+ "#{prefix}.#{sig}"
114
132
  end
115
133
 
116
134
  # Produce a digital signature of the hash contents, including the expiration timestamp
@@ -120,25 +138,8 @@ module RightSupport::Crypto
120
138
  # @param [Time] expires_at
121
139
  # @return [String] a binary signature of the hash's contents
122
140
  def sign(expires_at)
123
- raise ArgumentError, "Cannot sign; missing private_key" unless @private_key
124
- raise ArgumentError, "expires_at must be a Time in the future" unless time_check(expires_at)
125
-
126
- metadata = {:expires_at => expires_at}
127
- encoded = encode(canonicalize(frame(@hash, metadata)))
128
-
129
- if @envelope
130
- digest = DIGEST_MAP[@digest].new(encoded)
131
- if @private_key.respond_to?(:dsa_sign_asn1)
132
- # DSA signature with ASN.1 encoding
133
- @private_key.dsa_sign_asn1(digest.digest)
134
- else
135
- # RSA/DSA signature as specified in PKCS #1 v1.5
136
- @private_key.sign(digest, encoded)
137
- end
138
- else
139
- digest = @digest.new.update(encoded).digest
140
- @private_key.private_encrypt(digest)
141
- end
141
+ _, sig = sign_with_canonical_representation(expires_at)
142
+ sig
142
143
  end
143
144
 
144
145
  # Verify a digital signature of the hash's contents. In order for the signature to verify,
@@ -151,30 +152,48 @@ module RightSupport::Crypto
151
152
  # @raise [ExpiredSignature] if the signature is expired
152
153
  # @raise [InvalidSignature] if the signature is invalid
153
154
  def verify!(signature, expires_at)
154
- raise ArgumentError, "Cannot verify; missing public_key" unless @public_key
155
+ raise ArgumentError, "Cannot verify; missing public_key" unless @key
155
156
 
156
157
  metadata = {:expires_at => expires_at}
157
- plaintext = encode( canonicalize( frame(@hash, metadata) ) )
158
+ plaintext = encode(canonicalize(frame(@data, metadata)))
158
159
 
159
- if @envelope
160
+ case @envelope
161
+ when :none
162
+ digest = @digest.new.update(plaintext).digest
163
+ # raw RSA decryption
164
+ actual = @key.public_decrypt(signature)
165
+ raise InvalidSignature, "Signature mismatch: expected #{digest}, got #{actual}" unless actual == digest
166
+ when :jwt
167
+ if @key.respond_to?(:dsa_verify_asn1)
168
+ # DSA signature with JWT-compatible encoding
169
+ digest = @digest.new.update(plaintext).digest
170
+ signature = raw_to_asn1(signature, @key)
171
+ result = @key.dsa_verify_asn1(digest, signature)
172
+ raise InvalidSignature, "Signature mismatch: DSA verify failed" unless result
173
+ elsif @key.respond_to?(:verify)
174
+ digest = @digest.new
175
+ result = @key.verify(digest, signature, plaintext)
176
+ raise InvalidSignature, "Signature mismatch: verify failed" unless result
177
+ else
178
+ raise NotImplementedError, "Cannot verify JWT with #{@key.class.name}"
179
+ end
180
+ when :right_support
160
181
  digest = DIGEST_MAP[@digest].new
161
- if @public_key.respond_to?(:moo)
182
+ if @key.respond_to?(:dsa_verify_asn1)
162
183
  # DSA signature with ASN.1 encoding
163
- @private_key.dsa_verify_asn1(digest.digest, signature)
184
+ @key.dsa_verify_asn1(digest.digest, signature)
164
185
  else
165
186
  # RSA/DSA signature as specified in PKCS #1 v1.5
166
- result = @public_key.verify(digest, signature, plaintext)
187
+ result = @key.verify(digest, signature, plaintext)
167
188
  end
168
189
  raise InvalidSignature, "Signature verification failed" unless true == result
169
- else
170
- expected = @digest.new.update(plaintext).digest
171
- actual = @public_key.public_decrypt(signature)
172
- raise InvalidSignature, "Signature mismatch: expected #{expected}, got #{actual}" unless actual == expected
173
190
  end
174
191
 
175
192
  raise ExpiredSignature, "The signature has expired (or expires_at is not a Time)" unless time_check(expires_at)
176
193
 
177
194
  true
195
+ rescue OpenSSL::PKey::RSAError => e
196
+ raise InvalidSignature, "Signature mismatch: #{e.message}"
178
197
  end
179
198
 
180
199
  # Verify a digital signature of the hash's contents. In order for the signature to verify,
@@ -187,53 +206,131 @@ module RightSupport::Crypto
187
206
  # @return [false] if the signature or expiration failed to verify
188
207
  def verify(signature, expires_at)
189
208
  verify!(signature, expires_at)
190
- rescue Exception => e
209
+ rescue ExpiredSignature, InvalidSignature => e
191
210
  false
192
211
  end
193
212
 
194
213
  # Free the inner Hash.
195
214
  def method_missing(meth, *args)
196
- @hash.__send__(meth, *args)
215
+ @data.__send__(meth, *args)
197
216
  end
198
217
 
199
218
  # Free the inner Hash.
200
219
  def respond_to?(meth, include_all=false)
201
- super || @hash.respond_to?(meth)
220
+ super || @data.respond_to?(meth)
202
221
  end
203
222
 
204
223
  def respond_to_missing?(meth, include_all=false)
205
- super || @hash.respond_to?(meth, include_all)
224
+ super || @data.respond_to?(meth, include_all)
206
225
  end
207
226
 
208
227
  private
209
228
 
210
- def duck_type_check
229
+ # @return [Array] a pair of strings: the exact message that was signed, and its raw-binary signature
230
+ def sign_with_canonical_representation(expires_at)
231
+ raise ArgumentError, "Cannot sign; missing private_key" unless @key
232
+ raise ArgumentError, "expires_at must be a Time in the future" unless time_check(expires_at)
233
+
234
+ metadata = {:expires_at => expires_at}
235
+ plaintext = encode(canonicalize(frame(@data, metadata)))
236
+
237
+ case @envelope
238
+ when :none
239
+ digest = @digest.new.update(plaintext).digest
240
+ # raw RSA encryption; output is a single bignum
241
+ sig = @key.private_encrypt(digest)
242
+ when :jwt
243
+ if @key.respond_to?(:dsa_sign_asn1)
244
+ # raw ECDSA signature; output are two bignums (r, s) stuck together
245
+ # with no delimiter
246
+ digest = @digest.new.update(plaintext).digest
247
+ asn1 = @key.dsa_sign_asn1(digest)
248
+ sig = asn1_to_raw(asn1, @key)
249
+ elsif @key.respond_to?(:sign)
250
+ # RSA/DSA signature as specified in PKCS #1 v1.5
251
+ digest = @digest.new
252
+ sig = @key.sign(digest, plaintext)
253
+ else
254
+ raise NotImplementedError, "Cannot sign JWT with a #{@key.class.name}"
255
+ end
256
+ when :right_support
257
+ digest = DIGEST_MAP[@digest].new(plaintext)
258
+ if @key.respond_to?(:dsa_sign_asn1)
259
+ # DSA signature with ASN.1 encoding
260
+ sig = @key.dsa_sign_asn1(digest.digest)
261
+ else
262
+ # RSA/DSA signature as specified in PKCS #1 v1.5
263
+ sig = @key.sign(digest, plaintext)
264
+ end
265
+ end
266
+
267
+ return plaintext, sig
268
+ end
269
+
270
+ # @raise [ArgumentError] if there is anything wrong with the crypto parameters
271
+ def check_parameters
272
+ # raises if key/digest are not compatible with JOSE
273
+ begin
274
+ jose_header if @envelope == :jwt
275
+ rescue NotImplementedError => e
276
+ raise ArgumentError.new(e.message)
277
+ end
278
+
211
279
  unless DIGEST_MAP.key?(@digest)
212
- raise ArgumentError, "Digest must be a built-in Ruby Digest class: MD5, SHA1 or SHA2"
280
+ raise ArgumentError, "Digest must be one of Digest::* class or OpenSSL::Digest::*"
213
281
  end
214
- unless @encoding.respond_to?(str_or_symb('dump'))
282
+ unless @encoding.respond_to?(:dump)
215
283
  raise ArgumentError, "Encoding class/module/object must respond to .dump method"
216
284
  end
217
285
 
218
286
  if @envelope
219
- if @public_key && !@public_key.respond_to?(str_or_symb('verify'))
287
+ if @key && !@key.respond_to?(:verify)
220
288
  raise ArgumentError, "Public key must respond to :verify"
221
289
  end
222
- if @private_key && !@private_key.respond_to?(str_or_symb('sign'))
290
+ if @key && !@key.respond_to?(:sign)
223
291
  raise ArgumentError, "Private key must respond to :sign"
224
292
  end
225
293
  else
226
- if @public_key && !@public_key.respond_to?(str_or_symb('public_decrypt'))
294
+ if @key && !@key.respond_to?(:public_decrypt)
227
295
  raise ArgumentError, "Public key must respond to :public_decrypt"
228
296
  end
229
- if @private_key && !@private_key.respond_to?(str_or_symb('private_encrypt'))
297
+ if @key && !@key.respond_to?(:private_encrypt)
230
298
  raise ArgumentError, "Private key must respond to :private_encrypt"
231
299
  end
232
300
  end
233
301
  end
234
302
 
235
- def str_or_symb(method)
236
- RUBY_VERSION > '1.9' ? method.to_sym : method.to_s
303
+ # Figure out a default envelope if none is provided
304
+ def guess_envelope
305
+ case @key
306
+ when OpenSSL::PKey::EC
307
+ :jwt
308
+ else
309
+ :none
310
+ end
311
+ end
312
+
313
+ # Figure out a suitable default digest if none is provided
314
+ def guess_digest
315
+ case @key
316
+ when OpenSSL::PKey::DSA
317
+ OpenSSL::Digest::SHA1
318
+ when OpenSSL::PKey::EC
319
+ case @key.group.degree
320
+ when 256 then OpenSSL::Digest::SHA256
321
+ when 384 then OpenSSL::Digest::SHA384
322
+ when 521 then OpenSSL::Digest::SHA512
323
+ else
324
+ raise ArgumentError, "Cannot guess digest; please pass it as an option"
325
+ end
326
+ when OpenSSL::PKey::RSA
327
+ case @envelope
328
+ when :jwt then OpenSSL::Digest::SHA256
329
+ else OpenSSL::Digest::SHA1
330
+ end
331
+ else
332
+ OpenSSL::Digest::SHA1
333
+ end
237
334
  end
238
335
 
239
336
  # Ensure that an expiration time is in the future.
@@ -241,21 +338,28 @@ module RightSupport::Crypto
241
338
  t.is_a?(Time) && (t >= Time.now)
242
339
  end
243
340
 
244
- # Incorporate the hash and its signature metadata into an enclosing hash.
245
- def frame(data, metadata) # :nodoc:
246
- {:data => data, :metadata => metadata}
247
- end
248
-
249
341
  # Encode a canonicalized representation of the hash.
250
- def encode(input)
251
- @encoding.dump(input)
342
+ def encode(input) # :nodoc:
343
+ case @envelope
344
+ when :none, :right_support
345
+ @encoding.dump(input)
346
+ when :jwt
347
+ input.map { |m| RightSupport::Data::Base64URL.encode(m) }.join('.')
348
+ end
252
349
  end
253
350
 
254
- # Canonicalize the hash (and any nested data) by transforming it deterministically into a
255
- # structure of arrays-in-arrays whose elements are ordered according to the lexical ordering
256
- # of hash keys. Canonicalization ensures that the signer and verifier agree on the contents
257
- # of the thing being signed irrespective of Ruby version, CPU architecture, etc.
351
+ # If envelope is :jwt, return the input with each element mapped
352
+ # to its encoded form, but otherwise unchanged in any way.
353
+ #
354
+ # For any other envelope type, canonicalize the hash (and any nested data)
355
+ # by transforming it deterministically into a structure of arrays-in-arrays
356
+ # whose elements are ordered according to the lexical ordering of hash keys.
357
+ # Canonicalization ensures that the signer and verifier agree on the
358
+ # contents of the thing being signed irrespective of Ruby version, CPU
359
+ # architecture, etc.
258
360
  def canonicalize(input) # :nodoc:
361
+ return input.map { |i| @encoding.dump(i) } if @envelope == :jwt
362
+
259
363
  case input
260
364
  when Hash
261
365
  # Hash is the only complex case. We canonicalize a Hash as an Array of pairs, each of which
@@ -296,5 +400,59 @@ module RightSupport::Crypto
296
400
 
297
401
  output
298
402
  end
403
+
404
+ # Incorporate the hash and its signature metadata into a Hash or Array
405
+ # that represents the logical layout of the thing to be signed or verified.
406
+ def frame(data, metadata) # :nodoc:
407
+ case @envelope
408
+ when :none, :right_support
409
+ # Proprietary framing
410
+ {:data => data, :metadata => metadata}
411
+ when :jwt
412
+ # JOSE framing; see http://jose.readthedocs.io
413
+ payload = data.dup
414
+ payload['exp'] = metadata[:expires_at].to_i
415
+ [jose_header, payload]
416
+ end
417
+ end
418
+
419
+ # @return [Hash] JOSE header defining document type (JWT) and encryption algorithm
420
+ def jose_header
421
+ key = @key || @key
422
+
423
+ prefix = case key
424
+ when OpenSSL::PKey::EC
425
+ 'ES'
426
+ when OpenSSL::PKey::RSA
427
+ 'RS'
428
+ else
429
+ raise NotImplementedError, "Cannot use a #{key.class.name} with JWT envelope"
430
+ end
431
+
432
+ size = HASHSIZE_MAP[@digest]
433
+ unless size
434
+ raise NotImplementedError, "Cannot use #{@digest.name} with JWT envelope and this kind of key"
435
+ end
436
+
437
+ {'typ' => 'JWT', 'alg' => "#{prefix}#{size}"}
438
+ end
439
+
440
+ # Convert ASN1-encoded pair of integers into raw pair of concatenated bignums
441
+ # This only works for OpenSSL::PKey::EC.
442
+ # See https://github.com/jwt/ruby-jwt/blob/master/lib/jwt.rb#L166
443
+ def asn1_to_raw(signature, private_key) # :nodoc:
444
+ byte_size = (private_key.group.degree + 7) / 8
445
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
446
+ end
447
+
448
+ # Convert raw pair of concatenated bignums into ASN1-encoded pair of integers.
449
+ # This only works for OpenSSL::PKey::EC.
450
+ # https://github.com/jwt/ruby-jwt/blob/master/lib/jwt.rb#L159
451
+ def raw_to_asn1(signature, public_key) # :nodoc:
452
+ byte_size = (public_key.group.degree + 7) / 8
453
+ r = signature[0..(byte_size - 1)]
454
+ s = signature[byte_size..-1] || ''
455
+ OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
456
+ end
299
457
  end
300
458
  end
@@ -25,6 +25,10 @@ module RightSupport
25
25
  # A namespace for cryptographic functionality.
26
26
  #
27
27
  module Crypto
28
+ # Exception indicating that a cryptographic message payload contains
29
+ # something whose state can't cleanly signed, encrypted or verified.
30
+ class InvalidPayload < StandardError; end
31
+
28
32
  # Exception indicating that a cryptographic signature is invalid. This can happen for several
29
33
  # reasons:
30
34
  # * someone tampered with the signature
@@ -0,0 +1,27 @@
1
+ require 'base64'
2
+
3
+ module RightSupport::Data
4
+ # Implements the base64url encoding mechanism as described in RFC7515: JSON
5
+ # Web Signature (JWS).
6
+ #
7
+ # This encoding is similar to base64, but omits newlines and padding
8
+ # `=` characters, and uses the `-` and `_` characters in place of the `+` and
9
+ # `/` characters. This makes it suitable for use inside URLs, cookies, and
10
+ # other "webby" contexts where we want to avoid text expansion due to URL
11
+ # encoding.
12
+ module Base64URL
13
+ # @param [String] str ASCII string to encode
14
+ # @return [String] base64 representation of str
15
+ def self.encode(text)
16
+ Base64.encode64(text).tr('+/', '-_').gsub(/[\n=]/, '')
17
+ end
18
+
19
+ # @param [String] str base64 to decode to ASCII
20
+ # @return [String] plain ASCII
21
+ def self.decode(b64)
22
+ b64 = b64.dup
23
+ b64 += '=' * (4 - b64.length.modulo(4))
24
+ Base64.decode64(b64.tr('-_', '+/'))
25
+ end
26
+ end
27
+ end
@@ -35,3 +35,4 @@ require 'right_support/data/hash_tools'
35
35
  require 'right_support/data/uuid'
36
36
  require 'right_support/data/mash'
37
37
  require 'right_support/data/token'
38
+ require 'right_support/data/base64_url'
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module RightSupport
3
- VERSION = '2.14.0'.freeze
3
+ VERSION = '2.14.1'.freeze
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: right_support
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.14.0
4
+ version: 2.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Spataro
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2016-12-14 00:00:00.000000000 Z
16
+ date: 2016-12-20 00:00:00.000000000 Z
17
17
  dependencies: []
18
18
  description: A toolkit of useful, reusable foundation code created by RightScale.
19
19
  email: support@rightscale.com
@@ -33,6 +33,7 @@ files:
33
33
  - lib/right_support/crypto.rb
34
34
  - lib/right_support/crypto/signed_hash.rb
35
35
  - lib/right_support/data.rb
36
+ - lib/right_support/data/base64_url.rb
36
37
  - lib/right_support/data/hash_tools.rb
37
38
  - lib/right_support/data/mash.rb
38
39
  - lib/right_support/data/serializer.rb