sandal 0.3.0 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +1 -1
- data/lib/sandal.rb +89 -69
- data/lib/sandal/enc.rb +51 -0
- data/lib/sandal/enc/acbc_hs.rb +3 -16
- data/lib/sandal/enc/agcm.rb +1 -1
- data/lib/sandal/sig/hs.rb +1 -1
- data/lib/sandal/util.rb +1 -1
- data/lib/sandal/version.rb +1 -1
- data/spec/sandal/enc/a128cbc_hs256_spec.rb +3 -3
- data/spec/sandal/enc/a256gcm_spec.rb +3 -3
- data/spec/sandal/enc/alg/rsa1_5_spec.rb +40 -0
- data/spec/sandal/enc/shared_examples.rb +3 -3
- data/spec/sandal_spec.rb +30 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 03610644805fbd043ba5d03d61a1d5926b45d37f
|
4
|
+
data.tar.gz: 9622863e1097e93f74740cd112ae2dd4d98a8fd7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff2c734b2f07a064bad1701e26d976db390d9ebe2e666c41f066d80948486f7b16dca0ff189ba6772826dae589da2390195695fe3fa187e03b11cfc398325094
|
7
|
+
data.tar.gz: 77ea15a46b9170678cc34382f690e283ee0b1ec448b7e1caa29edfdfe053e5b41ee9615278687dd711c90bfd4828b0922714a6acd1ecce5e98ee27dd518c41a8
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## 0.4.0 (30 April 2013)
|
2
|
+
|
3
|
+
Features:
|
4
|
+
|
5
|
+
- The decode_token method now recursively decodes/decrypts nested tokens rather than requiring multiple calls.
|
6
|
+
|
7
|
+
Breaking changes:
|
8
|
+
|
9
|
+
- The decode_token method is now used for both signed and encrypted tokens; the decrypt_token method has been removed.
|
10
|
+
|
11
|
+
Bug fixes:
|
12
|
+
|
13
|
+
- The 'none' algorithm value is now always set in plaintext tokens.
|
14
|
+
- The zip parameter is now used when encrypting/decrypting JWE tokens.
|
15
|
+
- The Concat KDF function now works correctly when inputs have different string encodings by treating them all as binary.
|
16
|
+
- Errors related to jwt_base64_encode not being found when running under rack (although they worked fine under rspec) are resolved.
|
17
|
+
|
1
18
|
## 0.3.0 (20 April 2013)
|
2
19
|
|
3
20
|
Features:
|
data/README.md
CHANGED
@@ -83,7 +83,7 @@ jwe_token = Sandal.encrypt_token(jws_token, encrypter, {
|
|
83
83
|
Decrypting example:
|
84
84
|
|
85
85
|
```ruby
|
86
|
-
jws_token = Sandal.
|
86
|
+
jws_token = Sandal.decode_token(jwe_token) do |header|
|
87
87
|
if header['kid'] == 'your rsa key'
|
88
88
|
alg = Sandal::Enc::Alg::RSA_OAEP.new(File.Read('/path/to/rsa_private_key.pem'))
|
89
89
|
Sandal::Enc::A128GCM.new(alg)
|
data/lib/sandal.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'multi_json'
|
2
2
|
require 'openssl'
|
3
|
+
require 'zlib'
|
3
4
|
require 'sandal/version'
|
4
5
|
require 'sandal/claims'
|
5
6
|
require 'sandal/enc'
|
@@ -48,7 +49,7 @@ module Sandal
|
|
48
49
|
ignore_signature: false,
|
49
50
|
max_clock_skew: 0,
|
50
51
|
valid_iss: [],
|
51
|
-
valid_aud: []
|
52
|
+
valid_aud: []
|
52
53
|
}
|
53
54
|
|
54
55
|
# Overrides the default options.
|
@@ -60,6 +61,30 @@ module Sandal
|
|
60
61
|
DEFAULT_OPTIONS.merge!(defaults)
|
61
62
|
end
|
62
63
|
|
64
|
+
# Checks whether a token is encrypted.
|
65
|
+
#
|
66
|
+
# @param token [String or Array] The token, or token parts.
|
67
|
+
# @return [Boolean] true if the token is encrypted; otherwise false.
|
68
|
+
def self.is_encrypted?(token)
|
69
|
+
if token.is_a?(String)
|
70
|
+
token.count('.') == 4
|
71
|
+
else
|
72
|
+
token.count == 5
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Checks whether a token is signed.
|
77
|
+
#
|
78
|
+
# @param token [String or Array] The token, or token parts.
|
79
|
+
# @return [Boolean] true if the token is signed; otherwise false.
|
80
|
+
def self.is_signed?(token)
|
81
|
+
if token.is_a?(String)
|
82
|
+
!token.end_with?('.') && token.count('.') == 2
|
83
|
+
else
|
84
|
+
token.count == 3 && !token[2].nil? && !token[2].empty?
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
63
88
|
# Creates a signed JSON Web Token.
|
64
89
|
#
|
65
90
|
# @param payload [String or Hash] The payload of the token. Hashes will be
|
@@ -73,7 +98,7 @@ module Sandal
|
|
73
98
|
signer ||= Sandal::Sig::NONE
|
74
99
|
|
75
100
|
header = {}
|
76
|
-
header['alg'] = signer.name
|
101
|
+
header['alg'] = signer.name
|
77
102
|
header = header_fields.merge(header) if header_fields
|
78
103
|
header = MultiJson.dump(header)
|
79
104
|
|
@@ -84,41 +109,6 @@ module Sandal
|
|
84
109
|
[sec_input, jwt_base64_encode(signature)].join('.')
|
85
110
|
end
|
86
111
|
|
87
|
-
# Decodes and validates a signed JSON Web Token.
|
88
|
-
#
|
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.
|
93
|
-
#
|
94
|
-
# @param token [String] The encoded JSON Web Token.
|
95
|
-
# @yieldparam header [Hash] The JWT header values.
|
96
|
-
# @yieldparam options [Hash] (Optional) A hash that can be used to override
|
97
|
-
# the default options.
|
98
|
-
# @yieldreturn [#valid?] The signature validator.
|
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.
|
104
|
-
def self.decode_token(token)
|
105
|
-
parts = token.split('.')
|
106
|
-
header, payload, signature = decode_jws_token_parts(parts)
|
107
|
-
|
108
|
-
options = DEFAULT_OPTIONS.clone
|
109
|
-
validator = yield header, options if block_given?
|
110
|
-
validator ||= Sandal::Sig::NONE
|
111
|
-
|
112
|
-
unless options[:ignore_signature]
|
113
|
-
secured_input = parts.take(2).join('.')
|
114
|
-
unless validator.valid?(signature, secured_input)
|
115
|
-
raise TokenError, 'Invalid signature.'
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
parse_and_validate(payload, header['cty'], options)
|
120
|
-
end
|
121
|
-
|
122
112
|
# Creates an encrypted JSON Web Token.
|
123
113
|
#
|
124
114
|
# @param payload [String] The payload of the token.
|
@@ -132,53 +122,85 @@ module Sandal
|
|
132
122
|
header['alg'] = encrypter.alg.name
|
133
123
|
header = header_fields.merge(header) if header_fields
|
134
124
|
|
125
|
+
if header.has_key?('zip')
|
126
|
+
unless header['zip'] == 'DEF'
|
127
|
+
raise ArgumentError, 'Invalid zip algorithm.'
|
128
|
+
end
|
129
|
+
payload = Zlib::Deflate.deflate(payload, Zlib::BEST_COMPRESSION)
|
130
|
+
end
|
131
|
+
|
135
132
|
encrypter.encrypt(header, payload)
|
136
133
|
end
|
137
134
|
|
138
|
-
#
|
135
|
+
# Decodes and validates a signed and/or encrypted JSON Web Token, recursing
|
136
|
+
# into any nested tokens, and returns the payload.
|
139
137
|
#
|
140
138
|
# The block is called with the token header as the first parameter, and should
|
141
|
-
# return the appropriate
|
142
|
-
#
|
143
|
-
#
|
139
|
+
# return the appropriate signature or decryption method to either validate the
|
140
|
+
# signature or decrypt the token as applicable. When the tokens are nested,
|
141
|
+
# this block will be called once per token. It can optionally have a second
|
142
|
+
# options parameter which can be used to override the {DEFAULT_OPTIONS} on a
|
143
|
+
# per-token basis; options are not persisted between yields.
|
144
144
|
#
|
145
|
-
# @param token [String] The
|
145
|
+
# @param token [String] The encoded JSON Web Token.
|
146
|
+
# @param depth [Integer] The maximum depth of token nesting to decode to.
|
146
147
|
# @yieldparam header [Hash] The JWT header values.
|
147
|
-
# @yieldparam options [Hash] (Optional) A hash that can be used to override
|
148
|
+
# @yieldparam options [Hash] (Optional) A hash that can be used to override
|
148
149
|
# the default options.
|
149
|
-
# @yieldreturn [#decrypt] The token
|
150
|
-
#
|
150
|
+
# @yieldreturn [#valid? or #decrypt] The signature validator if the token is
|
151
|
+
# signed, or the token decrypter if the token is encrypted.
|
152
|
+
# @return [Hash or String] The payload of the token as a Hash if it was JSON,
|
151
153
|
# otherwise as a String.
|
152
154
|
# @raise [Sandal::ClaimError] One or more claims in the token is invalid.
|
153
|
-
# @raise [Sandal::TokenError] The token format is invalid, or
|
154
|
-
#
|
155
|
-
def self.
|
155
|
+
# @raise [Sandal::TokenError] The token format is invalid, or validation of
|
156
|
+
# the token failed.
|
157
|
+
def self.decode_token(token, depth = 16)
|
156
158
|
parts = token.split('.')
|
157
|
-
decoded_parts =
|
159
|
+
decoded_parts = decode_token_parts(parts)
|
158
160
|
header = decoded_parts[0]
|
159
161
|
|
160
162
|
options = DEFAULT_OPTIONS.clone
|
161
|
-
|
163
|
+
decoder = yield header, options if block_given?
|
164
|
+
|
165
|
+
if is_encrypted?(parts)
|
166
|
+
payload = decoder.decrypt(parts, decoded_parts)
|
167
|
+
if header.has_key?('zip')
|
168
|
+
unless header['zip'] == 'DEF'
|
169
|
+
raise Sandal::TokenError, 'Invalid zip algorithm.'
|
170
|
+
end
|
171
|
+
payload = Zlib::Inflate.inflate(payload)
|
172
|
+
end
|
173
|
+
else
|
174
|
+
payload = decoded_parts[1]
|
175
|
+
unless options[:ignore_signature]
|
176
|
+
validate_signature(parts, decoded_parts[2], decoder)
|
177
|
+
end
|
178
|
+
end
|
162
179
|
|
163
|
-
|
164
|
-
|
180
|
+
if header['cty'] == 'JWT'
|
181
|
+
if depth > 0
|
182
|
+
if block_given?
|
183
|
+
decode_token(payload, depth - 1, &Proc.new)
|
184
|
+
else
|
185
|
+
decode_token(payload, depth - 1)
|
186
|
+
end
|
187
|
+
else
|
188
|
+
payload
|
189
|
+
end
|
190
|
+
else
|
191
|
+
parse_and_validate(payload, options)
|
192
|
+
end
|
165
193
|
end
|
166
194
|
|
167
|
-
private
|
195
|
+
private
|
168
196
|
|
169
|
-
# Decodes
|
170
|
-
def self.
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
# Decodes the parts of a JWE token.
|
178
|
-
def self.decode_jwe_token_parts(parts)
|
179
|
-
parts = decode_token_parts(parts)
|
180
|
-
raise TokenError, 'Invalid token format.' unless parts.length == 5
|
181
|
-
parts
|
197
|
+
# Decodes and validates a signed JSON Web Token.
|
198
|
+
def self.validate_signature(parts, signature, validator)
|
199
|
+
validator ||= Sandal::Sig::NONE
|
200
|
+
secured_input = parts.take(2).join('.')
|
201
|
+
unless validator.valid?(signature, secured_input)
|
202
|
+
raise TokenError, 'Invalid signature.'
|
203
|
+
end
|
182
204
|
end
|
183
205
|
|
184
206
|
# Decodes the parts of a token.
|
@@ -191,9 +213,7 @@ private
|
|
191
213
|
end
|
192
214
|
|
193
215
|
# Parses the content of a token and validates the claims if is JSON claims.
|
194
|
-
def self.parse_and_validate(payload,
|
195
|
-
return payload if content_type == 'JWT'
|
196
|
-
|
216
|
+
def self.parse_and_validate(payload, options)
|
197
217
|
claims = MultiJson.load(payload) rescue nil
|
198
218
|
if claims
|
199
219
|
claims.extend(Sandal::Claims).validate_claims(options)
|
data/lib/sandal/enc.rb
CHANGED
@@ -1,6 +1,57 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
1
3
|
module Sandal
|
2
4
|
# Contains encryption (JWE) functionality.
|
3
5
|
module Enc
|
6
|
+
|
7
|
+
# The Concat Key Derivation Function.
|
8
|
+
#
|
9
|
+
# @param digest [OpenSSL::Digest or String] The digest for the algorithm.
|
10
|
+
# @param key [String] The key or shared secret.
|
11
|
+
# @param keydatalen [Integer] The desired output size in bits.
|
12
|
+
# @param algorithm_id [String] The name of the algorithm.
|
13
|
+
# @param party_u_info [String or 0] The partyUInfo.
|
14
|
+
# @param party_v_info [String or 0] The partyVInfo.
|
15
|
+
# @param supp_pub_info [String] Supplementary public info.
|
16
|
+
# @param supp_priv_info [String] Supplementary private info.
|
17
|
+
# @return [String] The derived keying material.
|
18
|
+
def self.concat_kdf(digest, key, keydatalen, algorithm_id,
|
19
|
+
party_u_info, party_v_info,
|
20
|
+
supp_pub_info = nil, supp_priv_info = nil)
|
21
|
+
digest = OpenSSL::Digest.new(digest) if digest.is_a?(String)
|
22
|
+
rounds = (keydatalen / (digest.digest_length * 8.0)).ceil
|
23
|
+
|
24
|
+
round_input = concat_kdf_round_input(key, keydatalen, algorithm_id,
|
25
|
+
party_u_info, party_v_info,
|
26
|
+
supp_pub_info, supp_priv_info)
|
27
|
+
|
28
|
+
(1..rounds).reduce('') do |output, round|
|
29
|
+
hash = digest.digest([round].pack('N') + round_input)
|
30
|
+
if round == rounds
|
31
|
+
round_bits = keydatalen % (digest.digest_length * 8)
|
32
|
+
hash = hash[0...(round_bits / 8)] unless round_bits == 0
|
33
|
+
end
|
34
|
+
output << hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# The round input for the Concat KDF function (excluding round number).
|
41
|
+
def self.concat_kdf_round_input(key, keydatalen, algorithm_id,
|
42
|
+
party_u_info, party_v_info,
|
43
|
+
supp_pub_info, supp_priv_info)
|
44
|
+
input = ''.force_encoding('binary')
|
45
|
+
input << key.force_encoding('binary')
|
46
|
+
input << [keydatalen].pack('N')
|
47
|
+
input << algorithm_id.force_encoding('binary')
|
48
|
+
input << (party_u_info == 0 ? [0].pack('N') : party_u_info.force_encoding('binary'))
|
49
|
+
input << (party_v_info == 0 ? [0].pack('N') : party_v_info.force_encoding('binary'))
|
50
|
+
input << supp_pub_info.force_encoding('binary') if supp_pub_info
|
51
|
+
input << supp_priv_info.force_encoding('binary') if supp_priv_info
|
52
|
+
input
|
53
|
+
end
|
54
|
+
|
4
55
|
end
|
5
56
|
end
|
6
57
|
|
data/lib/sandal/enc/acbc_hs.rb
CHANGED
@@ -7,7 +7,7 @@ module Sandal
|
|
7
7
|
# Base implementation of the AES/CBC+HMAC-SHA family of encryption
|
8
8
|
# algorithms.
|
9
9
|
class ACBC_HS
|
10
|
-
|
10
|
+
include Sandal::Util
|
11
11
|
|
12
12
|
# The JWA name of the encryption.
|
13
13
|
attr_reader :name
|
@@ -76,25 +76,12 @@ module Sandal
|
|
76
76
|
|
77
77
|
# Derives the content encryption key from the content master key.
|
78
78
|
def derive_encryption_key(cmk)
|
79
|
-
|
79
|
+
Sandal::Enc.concat_kdf(@digest, cmk, @aes_size, @name, 0, 0, 'Encryption')
|
80
80
|
end
|
81
81
|
|
82
82
|
# Derives the content integrity key from the content master key.
|
83
83
|
def derive_integrity_key(cmk)
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
# Derives content keys using the Concat KDF.
|
88
|
-
def derive_content_key(label, cmk, size)
|
89
|
-
hash_input = [1].pack('N')
|
90
|
-
hash_input << cmk
|
91
|
-
hash_input << [size].pack('N')
|
92
|
-
hash_input << @name.encode('utf-8')
|
93
|
-
hash_input << [0].pack('N')
|
94
|
-
hash_input << [0].pack('N')
|
95
|
-
hash_input << label.encode('us-ascii')
|
96
|
-
hash = @digest.digest(hash_input)
|
97
|
-
hash[0..((size / 8) - 1)]
|
84
|
+
Sandal::Enc.concat_kdf(@digest, cmk, @sha_size, @name, 0, 0, 'Integrity')
|
98
85
|
end
|
99
86
|
|
100
87
|
end
|
data/lib/sandal/enc/agcm.rb
CHANGED
data/lib/sandal/sig/hs.rb
CHANGED
data/lib/sandal/util.rb
CHANGED
@@ -22,7 +22,7 @@ module Sandal
|
|
22
22
|
def jwt_strings_equal?(a, b)
|
23
23
|
return true if a.object_id == b.object_id
|
24
24
|
return false if a.nil? || b.nil? || a.length != b.length
|
25
|
-
a.codepoints.zip(b.codepoints).reduce(0) { |r, (
|
25
|
+
a.codepoints.zip(b.codepoints).reduce(0) { |r, (x, y)| r |= x ^ y } == 0
|
26
26
|
end
|
27
27
|
|
28
28
|
# Base64 encodes a string, in compliance with the JWT specification.
|
data/lib/sandal/version.rb
CHANGED
@@ -19,7 +19,7 @@ describe Sandal::Enc::A128CBC_HS256 do
|
|
19
19
|
|
20
20
|
it 'can decrypt the example token' do
|
21
21
|
token = 'eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDK0hTMjU2In0.ZmnlqWgjXyqwjr7cXHys8F79anIUI6J2UWdAyRQEcGBU-KPHsePM910_RoTDGu1IW40Dn0dvcdVEjpJcPPNIbzWcMxDi131Ejeg-b8ViW5YX5oRdYdiR4gMSDDB3mbkInMNUFT-PK5CuZRnHB2rUK5fhPuF6XFqLLZCG5Q_rJm6Evex-XLcNQAJNa1-6CIU12Wj3mPExxw9vbnsQDU7B4BfmhdyiflLA7Ae5ZGoVRl3A__yLPXxRjHFhpOeDp_adx8NyejF5cz9yDKULugNsDMdlHeJQOMGVLYaSZt3KP6aWNSqFA1PHDg-10ceuTEtq_vPE4-Gtev4N4K4Eudlj4Q.AxY8DCtDaGlsbGljb3RoZQ.Rxsjg6PIExcmGSF7LnSEkDqWIKfAw1wZz2XpabV5PwQsolKwEauWYZNE9Q1hZJEZ.8LXqMd0JLGsxMaB5uoNaMpg7uUW_p40RlaZHCwMIyzk'
|
22
|
-
payload = Sandal.
|
22
|
+
payload = Sandal.decode_token(token) do |header|
|
23
23
|
alg = Sandal::Enc::Alg::RSA1_5.new(@rsa)
|
24
24
|
Sandal::Enc::A128CBC_HS256.new(alg)
|
25
25
|
end
|
@@ -28,7 +28,7 @@ describe Sandal::Enc::A128CBC_HS256 do
|
|
28
28
|
|
29
29
|
it 'raises a token error when the integrity value is changed' do
|
30
30
|
token = 'eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDK0hTMjU2In0.ZmnlqWgjXyqwjr7cXHys8F79anIUI6J2UWdAyRQEcGBU-KPHsePM910_RoTDGu1IW40Dn0dvcdVEjpJcPPNIbzWcMxDi131Ejeg-b8ViW5YX5oRdYdiR4gMSDDB3mbkInMNUFT-PK5CuZRnHB2rUK5fhPuF6XFqLLZCG5Q_rJm6Evex-XLcNQAJNa1-6CIU12Wj3mPExxw9vbnsQDU7B4BfmhdyiflLA7Ae5ZGoVRl3A__yLPXxRjHFhpOeDp_adx8NyejF5cz9yDKULugNsDMdlHeJQOMGVLYaSZt3KP6aWNSqFA1PHDg-10ceuTEtq_vPE4-Gtev4N4K4Eudlj4Q.AxY8DCtDaGlsbGljb3RoZQ.Rxsjg6PIExcmGSF7LnSEkDqWIKfAw1wZz2XpabV5PwQsolKwEauWYZNE9Q1hZJEZ.7V5ZDko0v_mf2PAc4JMiUg'
|
31
|
-
expect { Sandal.
|
31
|
+
expect { Sandal.decode_token(token) do |header|
|
32
32
|
alg = Sandal::Enc::Alg::RSA1_5.new(@rsa)
|
33
33
|
Sandal::Enc::A128CBC_HS256.new(alg)
|
34
34
|
end }.to raise_error Sandal::TokenError, 'Invalid integrity value.'
|
@@ -38,7 +38,7 @@ describe Sandal::Enc::A128CBC_HS256 do
|
|
38
38
|
|
39
39
|
it 'raises a token error when the RSA keys JWE section A.2 are changed' do
|
40
40
|
token = 'eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDK0hTMjU2In0.ZmnlqWgjXyqwjr7cXHys8F79anIUI6J2UWdAyRQEcGBU-KPHsePM910_RoTDGu1IW40Dn0dvcdVEjpJcPPNIbzWcMxDi131Ejeg-b8ViW5YX5oRdYdiR4gMSDDB3mbkInMNUFT-PK5CuZRnHB2rUK5fhPuF6XFqLLZCG5Q_rJm6Evex-XLcNQAJNa1-6CIU12Wj3mPExxw9vbnsQDU7B4BfmhdyiflLA7Ae5ZGoVRl3A__yLPXxRjHFhpOeDp_adx8NyejF5cz9yDKULugNsDMdlHeJQOMGVLYaSZt3KP6aWNSqFA1PHDg-10ceuTEtq_vPE4-Gtev4N4K4Eudlj4Q.AxY8DCtDaGlsbGljb3RoZQ.Rxsjg6PIExcmGSF7LnSEkDqWIKfAw1wZz2XpabV5PwQsolKwEauWYZNE9Q1hZJEZ.8LXqMd0JLGsxMaB5uoNaMpg7uUW_p40RlaZHCwMIyzk'
|
41
|
-
expect { Sandal.
|
41
|
+
expect { Sandal.decode_token(token) do |header|
|
42
42
|
rsa = OpenSSL::PKey::RSA.new(2048)
|
43
43
|
alg = Sandal::Enc::Alg::RSA1_5.new(rsa)
|
44
44
|
Sandal::Enc::A128CBC_HS256.new(alg)
|
@@ -21,7 +21,7 @@ describe Sandal::Enc::A256GCM do
|
|
21
21
|
|
22
22
|
it 'can decrypt the example token' do
|
23
23
|
token = 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.M2XxpbORKezKSzzQL_95-GjiudRBTqn_omS8z9xgoRb7L0Jw5UsEbxmtyHn2T71mrZLkjg4Mp8gbhYoltPkEOHvAopz25-vZ8C2e1cOaAo5WPcbSIuFcB4DjBOM3t0UAO6JHkWLuAEYoe58lcxIQneyKdaYSLbV9cKqoUoFQpvKWYRHZbfszIyfsa18rmgTjzrtLDTPnc09DSJE24aQ8w3i8RXEDthW9T1J6LsTH_vwHdwUgkI-tC2PNeGrnM-dNSfzF3Y7-lwcGy0FsdXkPXytvDV7y4pZeeUiQ-0VdibIN2AjjfW60nfrPuOjepMFG6BBBbR37pHcyzext9epOAQ.48V1_ALb6US04U3b._e21tGGhac_peEFkLXr2dMPUZiUkrw.7V5ZDko0v_mf2PAc4JMiUg'
|
24
|
-
payload = Sandal.
|
24
|
+
payload = Sandal.decode_token(token) do |header|
|
25
25
|
alg = Sandal::Enc::Alg::RSA_OAEP.new(@rsa)
|
26
26
|
Sandal::Enc::A256GCM.new(alg)
|
27
27
|
end
|
@@ -30,7 +30,7 @@ describe Sandal::Enc::A256GCM do
|
|
30
30
|
|
31
31
|
it 'raises a token error when the integrity value is changed' do
|
32
32
|
token = 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.M2XxpbORKezKSzzQL_95-GjiudRBTqn_omS8z9xgoRb7L0Jw5UsEbxmtyHn2T71mrZLkjg4Mp8gbhYoltPkEOHvAopz25-vZ8C2e1cOaAo5WPcbSIuFcB4DjBOM3t0UAO6JHkWLuAEYoe58lcxIQneyKdaYSLbV9cKqoUoFQpvKWYRHZbfszIyfsa18rmgTjzrtLDTPnc09DSJE24aQ8w3i8RXEDthW9T1J6LsTH_vwHdwUgkI-tC2PNeGrnM-dNSfzF3Y7-lwcGy0FsdXkPXytvDV7y4pZeeUiQ-0VdibIN2AjjfW60nfrPuOjepMFG6BBBbR37pHcyzext9epOAQ.48V1_ALb6US04U3b._e21tGGhac_peEFkLXr2dMPUZiUkrw.8LXqMd0JLGsxMaB5uoNaMpg7uUW_p40RlaZHCwMIyzk'
|
33
|
-
expect { Sandal.
|
33
|
+
expect { Sandal.decode_token(token) do |header|
|
34
34
|
alg = Sandal::Enc::Alg::RSA_OAEP.new(@rsa)
|
35
35
|
Sandal::Enc::A256GCM.new(alg)
|
36
36
|
end }.to raise_error Sandal::TokenError, 'Invalid token.'
|
@@ -40,7 +40,7 @@ describe Sandal::Enc::A256GCM do
|
|
40
40
|
|
41
41
|
it 'raises a token error when the RSA keys JWE section A.1 are changed' do
|
42
42
|
token = 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.M2XxpbORKezKSzzQL_95-GjiudRBTqn_omS8z9xgoRb7L0Jw5UsEbxmtyHn2T71mrZLkjg4Mp8gbhYoltPkEOHvAopz25-vZ8C2e1cOaAo5WPcbSIuFcB4DjBOM3t0UAO6JHkWLuAEYoe58lcxIQneyKdaYSLbV9cKqoUoFQpvKWYRHZbfszIyfsa18rmgTjzrtLDTPnc09DSJE24aQ8w3i8RXEDthW9T1J6LsTH_vwHdwUgkI-tC2PNeGrnM-dNSfzF3Y7-lwcGy0FsdXkPXytvDV7y4pZeeUiQ-0VdibIN2AjjfW60nfrPuOjepMFG6BBBbR37pHcyzext9epOAQ.48V1_ALb6US04U3b._e21tGGhac_peEFkLXr2dMPUZiUkrw.8LXqMd0JLGsxMaB5uoNaMpg7uUW_p40RlaZHCwMIyzk'
|
43
|
-
expect { Sandal.
|
43
|
+
expect { Sandal.decode_token(token) do |header|
|
44
44
|
rsa = OpenSSL::PKey::RSA.new(2048)
|
45
45
|
alg = Sandal::Enc::Alg::RSA_OAEP.new(rsa)
|
46
46
|
Sandal::Enc::A256GCM.new(alg)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
include Sandal::Util
|
5
|
+
|
6
|
+
describe Sandal::Enc::Alg::RSA1_5 do
|
7
|
+
|
8
|
+
context '#name' do
|
9
|
+
|
10
|
+
it 'is "RSA1_5"' do
|
11
|
+
alg = Sandal::Enc::Alg::RSA1_5.new(OpenSSL::PKey::RSA.new(2048))
|
12
|
+
alg.name.should == 'RSA1_5'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
context '#decrypt_cmk' do
|
18
|
+
|
19
|
+
it 'can decrypt the encypted content master key from JWE section A.2', :jruby_incompatible do
|
20
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
21
|
+
key.n = make_bn([177, 119, 33, 13, 164, 30, 108, 121, 207, 136, 107, 242, 12, 224, 19, 226, 198, 134, 17, 71, 173, 75, 42, 61, 48, 162, 206, 161, 97, 108, 185, 234, 226, 219, 118, 206, 118, 5, 169, 224, 60, 181, 90, 85, 51, 123, 6, 224, 4, 122, 29, 230, 151, 12, 244, 127, 121, 25, 4, 85, 220, 144, 215, 110, 130, 17, 68, 228, 129, 138, 7, 130, 231, 40, 212, 214, 17, 179, 28, 124, 151, 178, 207, 20, 14, 154, 222, 113, 176, 24, 198, 73, 211, 113, 9, 33, 178, 80, 13, 25, 21, 25, 153, 212, 206, 67, 154, 147, 70, 194, 192, 183, 160, 83, 98, 236, 175, 85, 23, 97, 75, 199, 177, 73, 145, 50, 253, 206, 32, 179, 254, 236, 190, 82, 73, 67, 129, 253, 252, 220, 108, 136, 138, 11, 192, 1, 36, 239, 228, 55, 81, 113, 17, 25, 140, 63, 239, 146, 3, 172, 96, 60, 227, 233, 64, 255, 224, 173, 225, 228, 229, 92, 112, 72, 99, 97, 26, 87, 187, 123, 46, 50, 90, 202, 117, 73, 10, 153, 47, 224, 178, 163, 77, 48, 46, 154, 33, 148, 34, 228, 33, 172, 216, 89, 46, 225, 127, 68, 146, 234, 30, 147, 54, 146, 5, 133, 45, 78, 254, 85, 55, 75, 213, 86, 194, 218, 215, 163, 189, 194, 54, 6, 83, 36, 18, 153, 53, 7, 48, 89, 35, 66, 144, 7, 65, 154, 13, 97, 75, 55, 230, 132, 3, 13, 239, 71])
|
22
|
+
key.e = make_bn([1, 0, 1])
|
23
|
+
key.d = make_bn([84, 80, 150, 58, 165, 235, 242, 123, 217, 55, 38, 154, 36, 181, 221, 156, 211, 215, 100, 164, 90, 88, 40, 228, 83, 148, 54, 122, 4, 16, 165, 48, 76, 194, 26, 107, 51, 53, 179, 165, 31, 18, 198, 173, 78, 61, 56, 97, 252, 158, 140, 80, 63, 25, 223, 156, 36, 203, 214, 252, 120, 67, 180, 167, 3, 82, 243, 25, 97, 214, 83, 133, 69, 16, 104, 54, 160, 200, 41, 83, 164, 187, 70, 153, 111, 234, 242, 158, 175, 28, 198, 48, 211, 45, 148, 58, 23, 62, 227, 74, 52, 117, 42, 90, 41, 249, 130, 154, 80, 119, 61, 26, 193, 40, 125, 10, 152, 174, 227, 225, 205, 32, 62, 66, 6, 163, 100, 99, 219, 19, 253, 25, 105, 80, 201, 29, 252, 157, 237, 69, 1, 80, 171, 167, 20, 196, 156, 109, 249, 88, 0, 3, 152, 38, 165, 72, 87, 6, 152, 71, 156, 214, 16, 71, 30, 82, 51, 103, 76, 218, 63, 9, 84, 163, 249, 91, 215, 44, 238, 85, 101, 240, 148, 1, 82, 224, 91, 135, 105, 127, 84, 171, 181, 152, 210, 183, 126, 24, 46, 196, 90, 173, 38, 245, 219, 186, 222, 27, 240, 212, 194, 15, 66, 135, 226, 178, 190, 52, 245, 74, 65, 224, 81, 100, 85, 25, 204, 165, 203, 187, 175, 84, 100, 82, 15, 11, 23, 202, 151, 107, 54, 41, 207, 3, 136, 229, 134, 131, 93, 139, 50, 182, 204, 93, 130, 89])
|
24
|
+
cmk = [4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, 207].pack('C*')
|
25
|
+
encrypted_cmk = jwt_base64_decode('ZmnlqWgjXyqwjr7cXHys8F79anIUI6J2UWdAyRQEcGBU-KPHsePM910_RoTDGu1IW40Dn0dvcdVEjpJcPPNIbzWcMxDi131Ejeg-b8ViW5YX5oRdYdiR4gMSDDB3mbkInMNUFT-PK5CuZRnHB2rUK5fhPuF6XFqLLZCG5Q_rJm6Evex-XLcNQAJNa1-6CIU12Wj3mPExxw9vbnsQDU7B4BfmhdyiflLA7Ae5ZGoVRl3A__yLPXxRjHFhpOeDp_adx8NyejF5cz9yDKULugNsDMdlHeJQOMGVLYaSZt3KP6aWNSqFA1PHDg-10ceuTEtq_vPE4-Gtev4N4K4Eudlj4Q')
|
26
|
+
alg = Sandal::Enc::Alg::RSA1_5.new(key)
|
27
|
+
alg.decrypt_cmk(encrypted_cmk).should == cmk
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'raises a TokenError when the wrong key is used for decryption' do
|
31
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
32
|
+
cmk = [4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, 207].pack('C*')
|
33
|
+
encrypted_cmk = jwt_base64_decode('ZmnlqWgjXyqwjr7cXHys8F79anIUI6J2UWdAyRQEcGBU-KPHsePM910_RoTDGu1IW40Dn0dvcdVEjpJcPPNIbzWcMxDi131Ejeg-b8ViW5YX5oRdYdiR4gMSDDB3mbkInMNUFT-PK5CuZRnHB2rUK5fhPuF6XFqLLZCG5Q_rJm6Evex-XLcNQAJNa1-6CIU12Wj3mPExxw9vbnsQDU7B4BfmhdyiflLA7Ae5ZGoVRl3A__yLPXxRjHFhpOeDp_adx8NyejF5cz9yDKULugNsDMdlHeJQOMGVLYaSZt3KP6aWNSqFA1PHDg-10ceuTEtq_vPE4-Gtev4N4K4Eudlj4Q')
|
34
|
+
alg = Sandal::Enc::Alg::RSA1_5.new(key)
|
35
|
+
expect { alg.decrypt_cmk(encrypted_cmk) }.to raise_error Sandal::TokenError, 'Cannot decrypt content master key.'
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -8,7 +8,7 @@ shared_examples 'algorithm compatibility' do |enc_class|
|
|
8
8
|
content_master_key = SecureRandom.random_bytes(32)
|
9
9
|
encrypter = enc_class.new(Sandal::Enc::Alg::Direct.new(content_master_key))
|
10
10
|
token = Sandal.encrypt_token(payload, encrypter)
|
11
|
-
output = Sandal.
|
11
|
+
output = Sandal.decode_token(token) { encrypter }
|
12
12
|
output.should == payload
|
13
13
|
end
|
14
14
|
|
@@ -17,7 +17,7 @@ shared_examples 'algorithm compatibility' do |enc_class|
|
|
17
17
|
rsa = OpenSSL::PKey::RSA.new(2048)
|
18
18
|
encrypter = enc_class.new(Sandal::Enc::Alg::RSA1_5.new(rsa.public_key))
|
19
19
|
token = Sandal.encrypt_token(payload, encrypter)
|
20
|
-
output = Sandal.
|
20
|
+
output = Sandal.decode_token(token) do
|
21
21
|
enc_class.new(Sandal::Enc::Alg::RSA1_5.new(rsa))
|
22
22
|
end
|
23
23
|
output.should == payload
|
@@ -28,7 +28,7 @@ shared_examples 'algorithm compatibility' do |enc_class|
|
|
28
28
|
rsa = OpenSSL::PKey::RSA.new(2048)
|
29
29
|
encrypter = enc_class.new(Sandal::Enc::Alg::RSA_OAEP.new(rsa.public_key))
|
30
30
|
token = Sandal.encrypt_token(payload, encrypter)
|
31
|
-
output = Sandal.
|
31
|
+
output = Sandal.decode_token(token) do
|
32
32
|
enc_class.new(Sandal::Enc::Alg::RSA_OAEP.new(rsa))
|
33
33
|
end
|
34
34
|
output.should == payload
|
data/spec/sandal_spec.rb
CHANGED
@@ -1,8 +1,38 @@
|
|
1
1
|
require 'helper'
|
2
2
|
require 'openssl'
|
3
|
+
require 'multi_json'
|
3
4
|
|
4
5
|
describe Sandal do
|
5
6
|
|
7
|
+
context '#encrypt_token' do
|
8
|
+
|
9
|
+
it 'supports zip using the DEFLATE algorithm' do
|
10
|
+
payload = 'some payload to be zipped'
|
11
|
+
private_key = OpenSSL::PKey::RSA.new(2048)
|
12
|
+
encrypter = Sandal::Enc::A128CBC_HS256.new(Sandal::Enc::Alg::RSA1_5.new(private_key.public_key))
|
13
|
+
token = Sandal.encrypt_token(payload, encrypter, { 'zip' => 'DEF' })
|
14
|
+
decoded_payload = Sandal.decode_token(token) do |header|
|
15
|
+
Sandal::Enc::A128CBC_HS256.new(Sandal::Enc::Alg::RSA1_5.new(private_key))
|
16
|
+
end
|
17
|
+
decoded_payload.should == payload
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'raises an ArgumentError if the zip parameter is present and nil' do
|
21
|
+
encrypter = Sandal::Enc::A128CBC_HS256.new(Sandal::Enc::Alg::RSA1_5.new(OpenSSL::PKey::RSA.new(2048)))
|
22
|
+
expect {
|
23
|
+
Sandal.encrypt_token('any payload', encrypter, { 'zip' => nil })
|
24
|
+
}.to raise_error ArgumentError, 'Invalid zip algorithm.'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'raises an ArgumentError if the zip parameter is present and not "DEF"' do
|
28
|
+
encrypter = Sandal::Enc::A128CBC_HS256.new(Sandal::Enc::Alg::RSA1_5.new(OpenSSL::PKey::RSA.new(2048)))
|
29
|
+
expect {
|
30
|
+
Sandal.encrypt_token('any payload', encrypter, { 'zip' => 'INVALID' })
|
31
|
+
}.to raise_error ArgumentError, 'Invalid zip algorithm.'
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
6
36
|
it 'raises a token error when the token format is invalid' do
|
7
37
|
expect { Sandal.decode_token('not a valid token') }.to raise_error Sandal::TokenError
|
8
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sandal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg Beech
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-04-
|
11
|
+
date: 2013-04-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multi_json
|
@@ -164,6 +164,7 @@ files:
|
|
164
164
|
- spec/sandal/enc/a256cbc_hs512_spec.rb
|
165
165
|
- spec/sandal/enc/a256gcm_spec.rb
|
166
166
|
- spec/sandal/enc/alg/direct_spec.rb
|
167
|
+
- spec/sandal/enc/alg/rsa1_5_spec.rb
|
167
168
|
- spec/sandal/enc/shared_examples.rb
|
168
169
|
- spec/sandal/sig/es_spec.rb
|
169
170
|
- spec/sandal/sig/hs_spec.rb
|
@@ -203,6 +204,7 @@ test_files:
|
|
203
204
|
- spec/sandal/enc/a256cbc_hs512_spec.rb
|
204
205
|
- spec/sandal/enc/a256gcm_spec.rb
|
205
206
|
- spec/sandal/enc/alg/direct_spec.rb
|
207
|
+
- spec/sandal/enc/alg/rsa1_5_spec.rb
|
206
208
|
- spec/sandal/enc/shared_examples.rb
|
207
209
|
- spec/sandal/sig/es_spec.rb
|
208
210
|
- spec/sandal/sig/hs_spec.rb
|