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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: be263e03b58c9fb37944f44b22935361d7ad7b3d
4
- data.tar.gz: f46dd2129ffe4870f9596aae4a19592a354f4aa4
3
+ metadata.gz: 0f0ff3d982eff5453f5a1db3592b96e6f8a75884
4
+ data.tar.gz: b2ea7fad0f11051f2470f1a90bec6803ee10bb09
5
5
  SHA512:
6
- metadata.gz: 59be234975d305a39bb25e7a1c508ac272d2bb10e5af132d9afe132c8c4419c555c8ec4e30b9cadc12c3a1e8eabbae57fb7f3d9bcb8309af322ea4df43d79457
7
- data.tar.gz: b680f42d362ae72b6c124efd7a06bedf1d2a38b7c214f7e587aef617c20c2ead8006f12b45150826d2fc2a1624206bf748a63a107d55465459169ef1c3423d90
6
+ metadata.gz: b10f36c1a03b93e428533b0df5eb3f4ba9ab18cfdd911ec3e1733f431b3b89c9babcd7cc2c4915787c6fbe9904427b4702ac5062db1f12439644e08be46c8fc0
7
+ data.tar.gz: 27de343a94a824e353b1118003c752596b408ab7b2a8381c15af1e079ec407b797518ae7e1b885845fe0a191f658ad0718fa8538265b80db038e19140bb3b807
data/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
- ## 0.2.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
- **NOTE: This library is pretty new and still has a lot of things that aren't finished or could be improved. It also needs much more testing against the specs. Expect bugs and interface changes. Pull requests or feedback very much appreciated.**
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
- gem 'sandal'
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
- key = OpenSSL::PKey::EC.new(File.read('/path/to/ec_private_key.pem'))
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
- key = OpenSSL::PKey::EC.new(File.read('/path/to/ec_public_key.pem'))
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; others will follow. Feel free to submit a pull request if you want to add one before I get around to it:
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
- key = OpenSSL::PKey::RSA.new(File.Read('/path/to/rsa_public_key.pem'))
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
- key = OpenSSL::PKey::RSA.new(File.Read('/path/to/rsa_private_key.pem'))
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! validate_signature: false, validate_exp: false
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
- # max_clock_skew:: The maximum clock skew, in seconds, when validating times.
22
- # valid_iss:: A list of valid token issuers, if issuer validation is required.
23
- # valid_aud:: A list of valid audiences, if audience validation is required.
24
- # validate_exp:: Whether the expiry date of the token is validated.
25
- # validate_nbf:: Whether the not-before date of the token is validated.
26
- # validate_signature:: Whether the signature of signed (JWS) tokens is validated.
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
- max_clock_skew: 300,
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 details).
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 (JWS).
63
+ # Creates a signed JSON Web Token.
45
64
  #
46
- # @param payload [String/Hash] The payload of the token. Hashes will be encoded as JSON.
47
- # @param signer [#name,#sign] The token signer, which may be nil for an unsigned token.
48
- # @param header_fields [Hash] Header fields for the token (note: do not include 'alg').
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::None.instance
73
+ signer ||= Sandal::Sig::NONE
52
74
 
53
75
  header = {}
54
- header['alg'] = signer.name if signer.name != Sandal::Sig::None.instance.name
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
- secured_input = [header, payload].map { |part| Sandal::Util.base64_encode(part) }.join('.')
61
- signature = signer.sign(secured_input)
62
- [secured_input, Sandal::Util.base64_encode(signature)].join('.')
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 (JWS).
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 return the appropriate
68
- # {Sandal::Sig} to validate the signature. It can optionally have a second options parameter which can
69
- # be used to override the {DEFAULT_OPTIONS} on a per-token basis.
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 the default options.
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/String] The payload of the token as a Hash if it was JSON, otherwise as a String.
76
- # @raise [Sandal::TokenError] The token format is invalid, or validation of the token failed.
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::None.instance
110
+ validator ||= Sandal::Sig::NONE
84
111
 
85
- if options[:validate_signature]
112
+ unless options[:ignore_signature]
86
113
  secured_input = parts.take(2).join('.')
87
- raise TokenError, 'Invalid signature.' unless validator.valid?(signature, secured_input)
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 (JWE).
122
+ # Creates an encrypted JSON Web Token.
94
123
  #
95
124
  # @param payload [String] The payload of the token.
96
- # @param encrypter [Sandal::Enc] The token encrypter.
97
- # @param header_fields [Hash] Header fields for the token (note: do not include 'alg' or 'enc').
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 (JWE).
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 the default options.
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/String] The payload of the token as a Hash if it was JSON, otherwise as a String.
115
- # @raise [Sandal::TokenError] The token format is invalid, or decryption/validation of the token failed.
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| Sandal::Util.base64_decode(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 a JSON claim set.
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 functionality.
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 {Sandal::DEFAULT_OPTIONS} for details).
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]) if options[:validate_exp]
12
- validate_nbf(options[:max_clock_skew]) if options[:validate_nbf]
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 expired.
23
- def validate_exp(max_clock_skew)
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 not valid yet.
36
- def validate_nbf(max_clock_skew)
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
- if valid_iss && valid_iss.length > 0
50
- raise Sandal::ClaimError, 'The issuer is invalid.' unless valid_iss.include?(self['iss'])
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 audience.
67
+ # @raise [Sandal::ClaimError] The 'aud' claim value does not contain a valid
68
+ # audience.
59
69
  def validate_aud(valid_aud)
60
- if valid_aud && valid_aud.length > 0
61
- aud = self['aud']
62
- aud = [aud] unless aud.is_a?(Array)
63
- raise Sandal::ClaimError, 'The audence is invalid.' unless (aud & valid_aud).length > 0
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
@@ -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 algorithms.
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 constructors.
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 key [#name, #encrypt_cmk, #decrypt_cmk] The algorithm to use to encrypt and/or decrypt the AES key.
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
- content_master_key = @alg.respond_to?(:cmk) ? @alg.cmk : cipher.random_key
33
- encrypted_key = @alg.encrypt_cmk(content_master_key)
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(content_master_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
- secured_parts = [MultiJson.dump(header), encrypted_key, iv, ciphertext]
40
- secured_input = secured_parts.map { |part| Sandal::Util.base64_encode(part) }.join('.')
41
- content_integrity_key = derive_integrity_key(content_master_key)
42
- integrity_value = compute_integrity_value(content_integrity_key, secured_input)
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
- secured_input << '.' << Sandal::Util.base64_encode(integrity_value)
48
+ sec_input << '.' << jwt_base64_encode(integrity_value)
45
49
  end
46
50
 
47
51
  def decrypt(parts, decoded_parts)
48
- content_master_key = @alg.decrypt_cmk(decoded_parts[1])
52
+ cmk = @alg.decrypt_cmk(decoded_parts[1])
49
53
 
50
- content_integrity_key = derive_integrity_key(content_master_key)
51
- computed_integrity_value = compute_integrity_value(content_integrity_key, parts.take(4).join('.'))
52
- raise Sandal::TokenError, 'Invalid integrity value.' unless decoded_parts[4] == computed_integrity_value
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
- cipher.key = derive_encryption_key(content_master_key)
56
- cipher.iv = decoded_parts[2]
57
- cipher.update(decoded_parts[3]) + cipher.final
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(content_integrity_key, secured_input)
64
- OpenSSL::HMAC.digest(@digest, content_integrity_key, secured_input)
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(content_master_key)
69
- derive_content_key('Encryption', content_master_key, @aes_size)
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(content_master_key)
74
- derive_content_key('Integrity', content_master_key, @sha_size)
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, content_master_key, size)
88
+ def derive_content_key(label, cmk, size)
79
89
  hash_input = [1].pack('N')
80
- hash_input << content_master_key
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')