sandal 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -1
- data/README.md +10 -25
- data/lib/sandal.rb +79 -40
- data/lib/sandal/claims.rb +30 -18
- data/lib/sandal/enc/acbc_hs.rb +36 -26
- data/lib/sandal/enc/agcm.rb +17 -11
- data/lib/sandal/enc/alg/direct.rb +5 -3
- data/lib/sandal/enc/alg/rsa1_5.rb +7 -4
- data/lib/sandal/enc/alg/rsa_oaep.rb +7 -4
- data/lib/sandal/sig.rb +4 -1
- data/lib/sandal/sig/es.rb +27 -18
- data/lib/sandal/sig/hs.rb +4 -2
- data/lib/sandal/sig/rs.rb +30 -12
- data/lib/sandal/util.rb +39 -20
- data/lib/sandal/version.rb +1 -1
- data/sandal.gemspec +1 -0
- data/spec/helper.rb +13 -1
- data/spec/sandal/claims_spec.rb +213 -0
- data/spec/sandal/enc/a128cbc_hs256_spec.rb +6 -42
- data/spec/sandal/enc/a128gcm_spec.rb +2 -15
- data/spec/sandal/enc/a256cbc_hs512_spec.rb +10 -0
- data/spec/sandal/enc/a256gcm_spec.rb +52 -0
- data/spec/sandal/enc/alg/direct_spec.rb +55 -0
- data/spec/sandal/enc/shared_examples.rb +37 -0
- data/spec/sandal/sig/es_spec.rb +86 -6
- data/spec/sandal/sig/rs_spec.rb +71 -0
- data/spec/sandal/util_spec.rb +62 -22
- data/spec/sandal_spec.rb +18 -6
- metadata +26 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f0ff3d982eff5453f5a1db3592b96e6f8a75884
|
4
|
+
data.tar.gz: b2ea7fad0f11051f2470f1a90bec6803ee10bb09
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b10f36c1a03b93e428533b0df5eb3f4ba9ab18cfdd911ec3e1733f431b3b89c9babcd7cc2c4915787c6fbe9904427b4702ac5062db1f12439644e08be46c8fc0
|
7
|
+
data.tar.gz: 27de343a94a824e353b1118003c752596b408ab7b2a8381c15af1e079ec407b797518ae7e1b885845fe0a191f658ad0718fa8538265b80db038e19140bb3b807
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,20 @@
|
|
1
|
-
## 0.
|
1
|
+
## 0.3.0 (20 April 2013)
|
2
|
+
|
3
|
+
Features:
|
4
|
+
|
5
|
+
- Keys can now be passed as strings as well as OpenSSL types.
|
6
|
+
|
7
|
+
Breaking changes:
|
8
|
+
|
9
|
+
- Default options have changed so that the behaviour is consistent with no options being passed.
|
10
|
+
|
11
|
+
Bug fixes:
|
12
|
+
|
13
|
+
- Strings are now compared by codepoint rather than by byte, in accordance with JWS § 5.3.
|
14
|
+
- Integrity value check in AES/CBC+HS algorithms now uses the constant time string comparison function rather than ==.
|
15
|
+
- Base64 decoding now checks that the decode was not lossy, as jruby would do a 'best effort' decode of invalid base64 strings.
|
16
|
+
|
17
|
+
## 0.2.0 (05 April 2013)
|
2
18
|
|
3
19
|
Features:
|
4
20
|
|
data/README.md
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
# Sandal [![Build Status](https://travis-ci.org/gregbeech/sandal.png?branch=master)](https://travis-ci.org/gregbeech/sandal) [![Coverage Status](https://coveralls.io/repos/gregbeech/sandal/badge.png?branch=master)](https://coveralls.io/r/gregbeech/sandal) [![Code Climate](https://codeclimate.com/github/gregbeech/sandal.png)](https://codeclimate.com/github/gregbeech/sandal)
|
1
|
+
# Sandal [![Build Status](https://travis-ci.org/gregbeech/sandal.png?branch=master)](https://travis-ci.org/gregbeech/sandal) [![Coverage Status](https://coveralls.io/repos/gregbeech/sandal/badge.png?branch=master)](https://coveralls.io/r/gregbeech/sandal) [![Code Climate](https://codeclimate.com/github/gregbeech/sandal.png)](https://codeclimate.com/github/gregbeech/sandal) [![Dependency Status](https://gemnasium.com/gregbeech/sandal.png)](https://gemnasium.com/gregbeech/sandal)
|
4
2
|
|
5
3
|
A Ruby library for creating and reading [JSON Web Tokens (JWT) draft-06](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06), supporting [JSON Web Signatures (JWS) draft-08](http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-08) and [JSON Web Encryption (JWE) draft-08](http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-08). See the [CHANGELOG](CHANGELOG.md) for version history.
|
6
4
|
|
@@ -8,7 +6,9 @@ A Ruby library for creating and reading [JSON Web Tokens (JWT) draft-06](http://
|
|
8
6
|
|
9
7
|
Add this line to your application's Gemfile:
|
10
8
|
|
11
|
-
|
9
|
+
```ruby
|
10
|
+
gem 'sandal'
|
11
|
+
```
|
12
12
|
|
13
13
|
And then execute:
|
14
14
|
|
@@ -30,16 +30,12 @@ All the JWA signature methods are supported:
|
|
30
30
|
Signing example:
|
31
31
|
|
32
32
|
```ruby
|
33
|
-
require 'openssl'
|
34
|
-
require 'sandal'
|
35
|
-
|
36
33
|
claims = {
|
37
34
|
'iss' => 'example.org',
|
38
35
|
'sub' => 'user@example.org',
|
39
36
|
'exp' => (Time.now + 3600).to_i
|
40
37
|
}
|
41
|
-
|
42
|
-
signer = Sandal::Sig::ES256.new(key)
|
38
|
+
signer = Sandal::Sig::ES256.new(File.read('/path/to/ec_private_key.pem'))
|
43
39
|
jws_token = Sandal.encode_token(claims, signer, {
|
44
40
|
'kid' => 'my ec key'
|
45
41
|
})
|
@@ -48,13 +44,9 @@ jws_token = Sandal.encode_token(claims, signer, {
|
|
48
44
|
Decoding and validating example:
|
49
45
|
|
50
46
|
```ruby
|
51
|
-
require 'openssl'
|
52
|
-
require 'sandal'
|
53
|
-
|
54
47
|
claims = Sandal.decode_token(jws_token) do |header|
|
55
48
|
if header['kid'] == 'my ec key'
|
56
|
-
|
57
|
-
Sandal::Sig::ES256.new(key)
|
49
|
+
Sandal::Sig::ES256.new(File.read('/path/to/ec_public_key.pem'))
|
58
50
|
end
|
59
51
|
end
|
60
52
|
```
|
@@ -66,14 +58,12 @@ Keys for these examples can be generated by executing:
|
|
66
58
|
|
67
59
|
## Encrypted Tokens
|
68
60
|
|
69
|
-
**NOTE: Encryption is still somewhat experimental and likely to be incorrect and/or buggy. Please let me know if you have any issues.**
|
70
|
-
|
71
61
|
All the JWA encryption methods are supported:
|
72
62
|
|
73
63
|
- A128CBC+HS256, A256CBC+HS512
|
74
64
|
- A128GCM, A256GCM (note: requires ruby 2.0.0 or later)
|
75
65
|
|
76
|
-
Some of the JWA key encryption algorithms are supported at the moment
|
66
|
+
Some of the JWA key encryption algorithms are supported at the moment. The key wrap algorithms don't appear to exist in Ruby so they're not likely to be supported in the near future, but ECDH-ES should be soon:
|
77
67
|
|
78
68
|
- RSA1_5
|
79
69
|
- RSA-OAEP
|
@@ -82,8 +72,7 @@ Some of the JWA key encryption algorithms are supported at the moment; others wi
|
|
82
72
|
Encrypting example (assumes use of the jws_token from the signing examples, as typically JWE tokens will be used to wrap JWS tokens):
|
83
73
|
|
84
74
|
```ruby
|
85
|
-
|
86
|
-
alg = Sandal::Enc::Alg::RSA_OAEP.new(key.public_key)
|
75
|
+
alg = Sandal::Enc::Alg::RSA_OAEP.new(File.Read('/path/to/rsa_public_key.pem'))
|
87
76
|
encrypter = Sandal::Enc::A128GCM.new(alg)
|
88
77
|
jwe_token = Sandal.encrypt_token(jws_token, encrypter, {
|
89
78
|
'kid': 'your rsa key',
|
@@ -94,13 +83,9 @@ jwe_token = Sandal.encrypt_token(jws_token, encrypter, {
|
|
94
83
|
Decrypting example:
|
95
84
|
|
96
85
|
```ruby
|
97
|
-
require 'openssl'
|
98
|
-
require 'sandal'
|
99
|
-
|
100
86
|
jws_token = Sandal.decrypt_token(jwe_token) do |header|
|
101
87
|
if header['kid'] == 'your rsa key'
|
102
|
-
|
103
|
-
alg = Sandal::Enc::Alg::RSA_OAEP.new(key)
|
88
|
+
alg = Sandal::Enc::Alg::RSA_OAEP.new(File.Read('/path/to/rsa_private_key.pem'))
|
104
89
|
Sandal::Enc::A128GCM.new(alg)
|
105
90
|
end
|
106
91
|
end
|
@@ -122,7 +107,7 @@ Sandal.default! valid_iss: ['example.org'], max_clock_skew: 60
|
|
122
107
|
Sometimes while developing it can be useful to turn off some validation options just to get things working (don't do this in production!):
|
123
108
|
|
124
109
|
```ruby
|
125
|
-
Sandal.default!
|
110
|
+
Sandal.default! ignore_signature: true, ignore_exp: true
|
126
111
|
```
|
127
112
|
|
128
113
|
These options can also be configured on a per-token basis by using a second `options` parameter in the block passed to the `decode` method.
|
data/lib/sandal.rb
CHANGED
@@ -7,8 +7,13 @@ require 'sandal/sig'
|
|
7
7
|
require 'sandal/util'
|
8
8
|
|
9
9
|
|
10
|
-
# A library for creating and reading JSON Web Tokens (JWT)
|
10
|
+
# A library for creating and reading JSON Web Tokens (JWT), supporting JSON Web
|
11
|
+
# Signatures (JWS) and JSON Web Encryption (JWE).
|
12
|
+
#
|
13
|
+
# Currently supports draft-06 of the JWT spec, and draft-08 of the JWS and JWE
|
14
|
+
# specs.
|
11
15
|
module Sandal
|
16
|
+
extend Sandal::Util
|
12
17
|
|
13
18
|
# The error that is raised when a token is invalid.
|
14
19
|
class TokenError < StandardError; end
|
@@ -18,83 +23,108 @@ module Sandal
|
|
18
23
|
|
19
24
|
# The default options for token handling.
|
20
25
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
26
|
+
# ignore_exp::
|
27
|
+
# Whether to ignore the expiry date of the token. This setting is just to
|
28
|
+
# help get things working and should always be false in real apps!
|
29
|
+
# ignore_nbf::
|
30
|
+
# Whether to ignore the not-before date of the token. This setting is just
|
31
|
+
# to help get things working and should always be false in real apps!
|
32
|
+
# ignore_signature::
|
33
|
+
# Whether to ignore the signature of signed (JWS) tokens. This setting is
|
34
|
+
# just tohelp get things working and should always be false in real apps!
|
35
|
+
# max_clock_skew::
|
36
|
+
# The maximum clock skew, in seconds, when validating times. If your server
|
37
|
+
# time is out of sync with the token server then this can be increased to
|
38
|
+
# take that into account. It probably shouldn't be more than about 300.
|
39
|
+
# valid_iss::
|
40
|
+
# A list of valid token issuers, if validation of the issuer claim is
|
41
|
+
# required.
|
42
|
+
# valid_aud::
|
43
|
+
# A list of valid audiences, if validation of the audience claim is
|
44
|
+
# required.
|
27
45
|
DEFAULT_OPTIONS = {
|
28
|
-
|
46
|
+
ignore_exp: false,
|
47
|
+
ignore_nbf: false,
|
48
|
+
ignore_signature: false,
|
49
|
+
max_clock_skew: 0,
|
29
50
|
valid_iss: [],
|
30
51
|
valid_aud: [],
|
31
|
-
validate_exp: true,
|
32
|
-
validate_nbf: true,
|
33
|
-
validate_signature: true
|
34
52
|
}
|
35
53
|
|
36
54
|
# Overrides the default options.
|
37
55
|
#
|
38
|
-
# @param defaults [Hash] The options to override (see {DEFAULT_OPTIONS} for
|
56
|
+
# @param defaults [Hash] The options to override (see {DEFAULT_OPTIONS} for
|
57
|
+
# details).
|
39
58
|
# @return [Hash] The new default options.
|
40
59
|
def self.default!(defaults)
|
41
60
|
DEFAULT_OPTIONS.merge!(defaults)
|
42
61
|
end
|
43
62
|
|
44
|
-
# Creates a signed JSON Web Token
|
63
|
+
# Creates a signed JSON Web Token.
|
45
64
|
#
|
46
|
-
# @param payload [String
|
47
|
-
#
|
48
|
-
# @param
|
65
|
+
# @param payload [String or Hash] The payload of the token. Hashes will be
|
66
|
+
# encoded as JSON.
|
67
|
+
# @param signer [#name,#sign] The token signer, which may be nil for an
|
68
|
+
# unsigned token.
|
69
|
+
# @param header_fields [Hash] Header fields for the token (note: do not
|
70
|
+
# include 'alg').
|
49
71
|
# @return [String] A signed JSON Web Token.
|
50
72
|
def self.encode_token(payload, signer, header_fields = nil)
|
51
|
-
signer ||= Sandal::Sig::
|
73
|
+
signer ||= Sandal::Sig::NONE
|
52
74
|
|
53
75
|
header = {}
|
54
|
-
header['alg'] = signer.name if signer.name != Sandal::Sig::
|
76
|
+
header['alg'] = signer.name if signer.name != Sandal::Sig::NONE.name
|
55
77
|
header = header_fields.merge(header) if header_fields
|
56
78
|
header = MultiJson.dump(header)
|
57
79
|
|
58
80
|
payload = MultiJson.dump(payload) unless payload.is_a?(String)
|
59
81
|
|
60
|
-
|
61
|
-
signature = signer.sign(
|
62
|
-
[
|
82
|
+
sec_input = [header, payload].map { |p| jwt_base64_encode(p) }.join('.')
|
83
|
+
signature = signer.sign(sec_input)
|
84
|
+
[sec_input, jwt_base64_encode(signature)].join('.')
|
63
85
|
end
|
64
86
|
|
65
|
-
# Decodes and validates a signed JSON Web Token
|
87
|
+
# Decodes and validates a signed JSON Web Token.
|
66
88
|
#
|
67
|
-
# The block is called with the token header as the first parameter, and should
|
68
|
-
#
|
69
|
-
# be used to override the
|
89
|
+
# The block is called with the token header as the first parameter, and should
|
90
|
+
# return the appropriate signature method to validate the signature. It can
|
91
|
+
# optionally have a second options parameter which can be used to override the
|
92
|
+
# {DEFAULT_OPTIONS} on a per-token basis.
|
70
93
|
#
|
71
94
|
# @param token [String] The encoded JSON Web Token.
|
72
95
|
# @yieldparam header [Hash] The JWT header values.
|
73
|
-
# @yieldparam options [Hash] (Optional) A hash that can be used to override
|
96
|
+
# @yieldparam options [Hash] (Optional) A hash that can be used to override
|
97
|
+
# the default options.
|
74
98
|
# @yieldreturn [#valid?] The signature validator.
|
75
|
-
# @return [Hash
|
76
|
-
#
|
99
|
+
# @return [Hash or String] The payload of the token as a Hash if it was JSON,
|
100
|
+
# otherwise as a String.
|
101
|
+
# @raise [Sandal::ClaimError] One or more claims in the token is invalid.
|
102
|
+
# @raise [Sandal::TokenError] The token format is invalid, or validation of
|
103
|
+
# the token failed.
|
77
104
|
def self.decode_token(token)
|
78
105
|
parts = token.split('.')
|
79
106
|
header, payload, signature = decode_jws_token_parts(parts)
|
80
107
|
|
81
108
|
options = DEFAULT_OPTIONS.clone
|
82
109
|
validator = yield header, options if block_given?
|
83
|
-
validator ||= Sandal::Sig::
|
110
|
+
validator ||= Sandal::Sig::NONE
|
84
111
|
|
85
|
-
|
112
|
+
unless options[:ignore_signature]
|
86
113
|
secured_input = parts.take(2).join('.')
|
87
|
-
|
114
|
+
unless validator.valid?(signature, secured_input)
|
115
|
+
raise TokenError, 'Invalid signature.'
|
116
|
+
end
|
88
117
|
end
|
89
118
|
|
90
119
|
parse_and_validate(payload, header['cty'], options)
|
91
120
|
end
|
92
121
|
|
93
|
-
# Creates an encrypted JSON Web Token
|
122
|
+
# Creates an encrypted JSON Web Token.
|
94
123
|
#
|
95
124
|
# @param payload [String] The payload of the token.
|
96
|
-
# @param encrypter [
|
97
|
-
# @param header_fields [Hash] Header fields for the token (note: do not
|
125
|
+
# @param encrypter [#name,#alg,#encrypt] The token encrypter.
|
126
|
+
# @param header_fields [Hash] Header fields for the token (note: do not
|
127
|
+
# include 'alg' or 'enc').
|
98
128
|
# @return [String] An encrypted JSON Web Token.
|
99
129
|
def self.encrypt_token(payload, encrypter, header_fields = nil)
|
100
130
|
header = {}
|
@@ -105,14 +135,23 @@ module Sandal
|
|
105
135
|
encrypter.encrypt(header, payload)
|
106
136
|
end
|
107
137
|
|
108
|
-
# Decrypts and validates an encrypted JSON Web Token
|
138
|
+
# Decrypts and validates an encrypted JSON Web Token.
|
139
|
+
#
|
140
|
+
# The block is called with the token header as the first parameter, and should
|
141
|
+
# return the appropriate encryption method to decrypt the token. It can
|
142
|
+
# optionally have a second options parameter which can be used to override the
|
143
|
+
# {DEFAULT_OPTIONS} on a per-token basis.
|
109
144
|
#
|
110
145
|
# @param token [String] The encrypted JSON Web Token.
|
111
146
|
# @yieldparam header [Hash] The JWT header values.
|
112
|
-
# @yieldparam options [Hash] (Optional) A hash that can be used to override
|
147
|
+
# @yieldparam options [Hash] (Optional) A hash that can be used to override
|
148
|
+
# the default options.
|
113
149
|
# @yieldreturn [#decrypt] The token decrypter.
|
114
|
-
# @return [Hash
|
115
|
-
#
|
150
|
+
# @return [Hash or String] The payload of the token as a Hash if it was JSON,
|
151
|
+
# otherwise as a String.
|
152
|
+
# @raise [Sandal::ClaimError] One or more claims in the token is invalid.
|
153
|
+
# @raise [Sandal::TokenError] The token format is invalid, or decryption or
|
154
|
+
# validation of the token failed.
|
116
155
|
def self.decrypt_token(token)
|
117
156
|
parts = token.split('.')
|
118
157
|
decoded_parts = decode_jwe_token_parts(parts)
|
@@ -144,14 +183,14 @@ private
|
|
144
183
|
|
145
184
|
# Decodes the parts of a token.
|
146
185
|
def self.decode_token_parts(parts)
|
147
|
-
parts = parts.map { |part|
|
186
|
+
parts = parts.map { |part| jwt_base64_decode(part) }
|
148
187
|
parts[0] = MultiJson.load(parts[0])
|
149
188
|
parts
|
150
189
|
rescue
|
151
190
|
raise TokenError, 'Invalid token encoding.'
|
152
191
|
end
|
153
192
|
|
154
|
-
# Parses the content of a token and validates the claims if is
|
193
|
+
# Parses the content of a token and validates the claims if is JSON claims.
|
155
194
|
def self.parse_and_validate(payload, content_type, options)
|
156
195
|
return payload if content_type == 'JWT'
|
157
196
|
|
data/lib/sandal/claims.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
module Sandal
|
2
|
-
# A module that can be mixed into Hash-like objects to provide claims-related
|
2
|
+
# A module that can be mixed into Hash-like objects to provide claims-related
|
3
|
+
# functionality.
|
3
4
|
module Claims
|
4
5
|
|
5
6
|
# Validates the set of claims.
|
6
7
|
#
|
7
|
-
# @param options [Hash] The validation options (see
|
8
|
+
# @param options [Hash] The claim validation options (see
|
9
|
+
# {Sandal::DEFAULT_OPTIONS} for details).
|
8
10
|
# @return [Hash] A reference to self.
|
9
11
|
# @raise [Sandal::ClaimError] One or more claims is invalid.
|
10
|
-
def validate_claims(options)
|
11
|
-
validate_exp(options[:max_clock_skew])
|
12
|
-
validate_nbf(options[:max_clock_skew])
|
12
|
+
def validate_claims(options = {})
|
13
|
+
validate_exp(options[:max_clock_skew]) unless options[:ignore_exp]
|
14
|
+
validate_nbf(options[:max_clock_skew]) unless options[:ignore_nbf]
|
13
15
|
validate_iss(options[:valid_iss])
|
14
16
|
validate_aud(options[:valid_aud])
|
15
17
|
self
|
@@ -19,21 +21,26 @@ module Sandal
|
|
19
21
|
#
|
20
22
|
# @param max_clock_skew [Numeric] The maximum clock skew, in seconds.
|
21
23
|
# @return [void].
|
22
|
-
# @raise [Sandal::ClaimError] The 'exp' claim is invalid, or the token has
|
23
|
-
|
24
|
+
# @raise [Sandal::ClaimError] The 'exp' claim is invalid, or the token has
|
25
|
+
# expired.
|
26
|
+
def validate_exp(max_clock_skew = 0)
|
27
|
+
max_clock_skew ||= 0
|
28
|
+
|
24
29
|
exp = time_claim('exp')
|
25
30
|
if exp && exp <= (Time.now - max_clock_skew)
|
26
31
|
raise Sandal::ClaimError, 'The token has expired.'
|
27
32
|
end
|
28
|
-
nil
|
29
33
|
end
|
30
34
|
|
31
35
|
# Validates the not-before claim.
|
32
36
|
#
|
33
37
|
# @param max_clock_skew [Numeric] The maximum clock skew, in seconds.
|
34
38
|
# @return [void].
|
35
|
-
# @raise [Sandal::ClaimError] The 'nbf' claim is invalid, or the token is
|
36
|
-
|
39
|
+
# @raise [Sandal::ClaimError] The 'nbf' claim is invalid, or the token is
|
40
|
+
# not valid yet.
|
41
|
+
def validate_nbf(max_clock_skew = 0)
|
42
|
+
max_clock_skew ||= 0
|
43
|
+
|
37
44
|
nbf = time_claim('nbf')
|
38
45
|
if nbf && nbf > (Time.now + max_clock_skew)
|
39
46
|
raise Sandal::ClaimError, 'The token is not valid yet.'
|
@@ -46,8 +53,10 @@ module Sandal
|
|
46
53
|
# @return [void].
|
47
54
|
# @raise [Sandal::ClaimError] The 'iss' claim value is not a valid issuer.
|
48
55
|
def validate_iss(valid_iss)
|
49
|
-
|
50
|
-
|
56
|
+
return unless valid_iss && valid_iss.length > 0
|
57
|
+
|
58
|
+
unless valid_iss.include?(self['iss'])
|
59
|
+
raise Sandal::ClaimError, 'The issuer is invalid.'
|
51
60
|
end
|
52
61
|
end
|
53
62
|
|
@@ -55,12 +64,15 @@ module Sandal
|
|
55
64
|
#
|
56
65
|
# @param valid_aud [Array] The valid audiences.
|
57
66
|
# @return [void].
|
58
|
-
# @raise [Sandal::ClaimError] The 'aud' claim value does not contain a valid
|
67
|
+
# @raise [Sandal::ClaimError] The 'aud' claim value does not contain a valid
|
68
|
+
# audience.
|
59
69
|
def validate_aud(valid_aud)
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
70
|
+
return unless valid_aud && valid_aud.length > 0
|
71
|
+
|
72
|
+
aud = self['aud']
|
73
|
+
aud = [aud] unless aud.is_a?(Array)
|
74
|
+
unless (aud & valid_aud).length > 0
|
75
|
+
raise Sandal::ClaimError, 'The audence is invalid.'
|
64
76
|
end
|
65
77
|
end
|
66
78
|
|
@@ -73,7 +85,7 @@ module Sandal
|
|
73
85
|
begin
|
74
86
|
Time.at(claim)
|
75
87
|
rescue
|
76
|
-
raise ClaimError, "The '#{name}' claim is invalid."
|
88
|
+
raise Sandal::ClaimError, "The '#{name}' claim is invalid."
|
77
89
|
end
|
78
90
|
end
|
79
91
|
end
|
data/lib/sandal/enc/acbc_hs.rb
CHANGED
@@ -4,8 +4,10 @@ require 'sandal/util'
|
|
4
4
|
module Sandal
|
5
5
|
module Enc
|
6
6
|
|
7
|
-
# Base implementation of the AES/CBC+HMAC-SHA family of encryption
|
7
|
+
# Base implementation of the AES/CBC+HMAC-SHA family of encryption
|
8
|
+
# algorithms.
|
8
9
|
class ACBC_HS
|
10
|
+
extend Sandal::Util
|
9
11
|
|
10
12
|
# The JWA name of the encryption.
|
11
13
|
attr_reader :name
|
@@ -13,11 +15,13 @@ module Sandal
|
|
13
15
|
# The JWA algorithm used to encrypt the content master key.
|
14
16
|
attr_reader :alg
|
15
17
|
|
16
|
-
# Creates a new instance; it's probably easier to use one of the subclass
|
18
|
+
# Creates a new instance; it's probably easier to use one of the subclass
|
19
|
+
# constructors.
|
17
20
|
#
|
18
21
|
# @param aes_size [Integer] The size of the AES algorithm.
|
19
22
|
# @param sha_size [Integer] The size of the SHA algorithm.
|
20
|
-
# @param
|
23
|
+
# @param alg [#name, #encrypt_cmk, #decrypt_cmk] The algorithm to use to
|
24
|
+
# encrypt and/or decrypt the AES key.
|
21
25
|
def initialize(aes_size, sha_size, alg)
|
22
26
|
@aes_size = aes_size
|
23
27
|
@sha_size = sha_size
|
@@ -29,55 +33,61 @@ module Sandal
|
|
29
33
|
|
30
34
|
def encrypt(header, payload)
|
31
35
|
cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
|
32
|
-
|
33
|
-
encrypted_key = @alg.encrypt_cmk(
|
36
|
+
cmk = @alg.respond_to?(:cmk) ? @alg.cmk : cipher.random_key
|
37
|
+
encrypted_key = @alg.encrypt_cmk(cmk)
|
34
38
|
|
35
|
-
cipher.key = derive_encryption_key(
|
39
|
+
cipher.key = derive_encryption_key(cmk)
|
36
40
|
iv = cipher.random_iv
|
37
41
|
ciphertext = cipher.update(payload) + cipher.final
|
38
42
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
integrity_value = compute_integrity_value(
|
43
|
+
sec_parts = [MultiJson.dump(header), encrypted_key, iv, ciphertext]
|
44
|
+
sec_input = sec_parts.map { |part| jwt_base64_encode(part) }.join('.')
|
45
|
+
cik = derive_integrity_key(cmk)
|
46
|
+
integrity_value = compute_integrity_value(cik, sec_input)
|
43
47
|
|
44
|
-
|
48
|
+
sec_input << '.' << jwt_base64_encode(integrity_value)
|
45
49
|
end
|
46
50
|
|
47
51
|
def decrypt(parts, decoded_parts)
|
48
|
-
|
52
|
+
cmk = @alg.decrypt_cmk(decoded_parts[1])
|
49
53
|
|
50
|
-
|
51
|
-
|
52
|
-
|
54
|
+
cik = derive_integrity_key(cmk)
|
55
|
+
integrity_value = compute_integrity_value(cik, parts.take(4).join('.'))
|
56
|
+
unless jwt_strings_equal?(decoded_parts[4], integrity_value)
|
57
|
+
raise Sandal::TokenError, 'Invalid integrity value.'
|
58
|
+
end
|
53
59
|
|
54
60
|
cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
|
55
|
-
|
56
|
-
|
57
|
-
|
61
|
+
begin
|
62
|
+
cipher.key = derive_encryption_key(cmk)
|
63
|
+
cipher.iv = decoded_parts[2]
|
64
|
+
cipher.update(decoded_parts[3]) + cipher.final
|
65
|
+
rescue OpenSSL::Cipher::CipherError
|
66
|
+
raise Sandal::TokenError, 'Invalid token.'
|
67
|
+
end
|
58
68
|
end
|
59
69
|
|
60
70
|
private
|
61
71
|
|
62
72
|
# Computes the integrity value.
|
63
|
-
def compute_integrity_value(
|
64
|
-
OpenSSL::HMAC.digest(@digest,
|
73
|
+
def compute_integrity_value(cik, sec_input)
|
74
|
+
OpenSSL::HMAC.digest(@digest, cik, sec_input)
|
65
75
|
end
|
66
76
|
|
67
77
|
# Derives the content encryption key from the content master key.
|
68
|
-
def derive_encryption_key(
|
69
|
-
derive_content_key('Encryption',
|
78
|
+
def derive_encryption_key(cmk)
|
79
|
+
derive_content_key('Encryption', cmk, @aes_size)
|
70
80
|
end
|
71
81
|
|
72
82
|
# Derives the content integrity key from the content master key.
|
73
|
-
def derive_integrity_key(
|
74
|
-
derive_content_key('Integrity',
|
83
|
+
def derive_integrity_key(cmk)
|
84
|
+
derive_content_key('Integrity', cmk, @sha_size)
|
75
85
|
end
|
76
86
|
|
77
87
|
# Derives content keys using the Concat KDF.
|
78
|
-
def derive_content_key(label,
|
88
|
+
def derive_content_key(label, cmk, size)
|
79
89
|
hash_input = [1].pack('N')
|
80
|
-
hash_input <<
|
90
|
+
hash_input << cmk
|
81
91
|
hash_input << [size].pack('N')
|
82
92
|
hash_input << @name.encode('utf-8')
|
83
93
|
hash_input << [0].pack('N')
|