right_support 2.14.0 → 2.14.1

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 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