sandal 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +6 -14
- data/CHANGELOG.md +11 -0
- data/README.md +55 -12
- data/lib/sandal/enc/acbc_hs.rb +107 -0
- data/lib/sandal/enc/agcm.rb +66 -0
- data/lib/sandal/enc/alg/direct.rb +48 -0
- data/lib/sandal/enc/alg/rsa1_5.rb +44 -0
- data/lib/sandal/enc/alg/rsa_oaep.rb +45 -0
- data/lib/sandal/enc/alg.rb +11 -0
- data/lib/sandal/enc.rb +4 -20
- data/lib/sandal/sig.rb +1 -1
- data/lib/sandal/version.rb +1 -1
- data/lib/sandal.rb +67 -63
- data/spec/helper.rb +10 -3
- data/spec/sandal/enc/a128cbc_hs256_spec.rb +85 -0
- data/spec/sandal/enc/a128gcm_spec.rb +26 -0
- data/spec/sandal/sig/es_spec.rb +1 -6
- metadata +26 -18
- data/lib/sandal/enc/aescbc.rb +0 -91
- data/lib/sandal/enc/aesgcm.rb +0 -40
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
YTBlODVkNGFiNWQwNGQzZWE2YjFkY2JmNTg4MDliMDlkN2YyYjA0NzBmYWYw
|
10
|
-
OTk5MjMzMjViYjNiODBjOGU0YmQ5MjlmMzUxODI2MzU4NzU5ZTI3ODQ1ZTk5
|
11
|
-
YmI1OThiMDFkZDNjNzRlMmViZTUxYjU5MWU0M2MwMGE4NDcxMDg=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
ZWFhNjNlNzc4NDgwNmI5OTAwMmNhNTFlNDE1NjhjY2ViM2E2N2FlMjQ5NjM3
|
14
|
-
MDkxZDk5MmFiMzY2NGM4YTlkMjY0MzI5NDI1YTRlYWIzOWY1ZTA2MzE2NzMw
|
15
|
-
NTIwMjA1ZjQxMTQ1ZWU3Y2I2YjA3YmI5NDA3OTgxODZhMGRkMjA=
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: be263e03b58c9fb37944f44b22935361d7ad7b3d
|
4
|
+
data.tar.gz: f46dd2129ffe4870f9596aae4a19592a354f4aa4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 59be234975d305a39bb25e7a1c508ac272d2bb10e5af132d9afe132c8c4419c555c8ec4e30b9cadc12c3a1e8eabbae57fb7f3d9bcb8309af322ea4df43d79457
|
7
|
+
data.tar.gz: b680f42d362ae72b6c124efd7a06bedf1d2a38b7c214f7e587aef617c20c2ead8006f12b45150826d2fc2a1624206bf748a63a107d55465459169ef1c3423d90
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## 0.2.0
|
2
|
+
|
3
|
+
Features:
|
4
|
+
|
5
|
+
- Added support for AES/CBC and AES/GCM encryption methods.
|
6
|
+
- Added RSA1_5, RSA-OAEP and direct key protection algorithms.
|
7
|
+
|
8
|
+
Bug fixes:
|
9
|
+
|
10
|
+
- Sandal::Sig::ES class is now not included in jruby as ECDSA isn't supported.
|
11
|
+
|
1
12
|
## 0.1.1 (01 April 2013)
|
2
13
|
|
3
14
|
Features:
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
**NOTE: This library is pretty new and still has a lot of things that aren't finished or could be improved. Expect bugs and interface changes. Pull requests or feedback very much appreciated.**
|
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
2
|
|
3
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)
|
4
4
|
|
5
|
-
A Ruby library for creating and reading [JSON Web Tokens (JWT)
|
5
|
+
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
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -20,7 +20,7 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Signed Tokens
|
22
22
|
|
23
|
-
|
23
|
+
All the JWA signature methods are supported:
|
24
24
|
|
25
25
|
- ES256, ES384, ES512 (note: not supported on jruby)
|
26
26
|
- HS256, HS384, HS512
|
@@ -38,10 +38,10 @@ claims = {
|
|
38
38
|
'sub' => 'user@example.org',
|
39
39
|
'exp' => (Time.now + 3600).to_i
|
40
40
|
}
|
41
|
-
key = OpenSSL::PKey::EC.new(File.read('/path/to/
|
41
|
+
key = OpenSSL::PKey::EC.new(File.read('/path/to/ec_private_key.pem'))
|
42
42
|
signer = Sandal::Sig::ES256.new(key)
|
43
|
-
|
44
|
-
'kid' => 'my key
|
43
|
+
jws_token = Sandal.encode_token(claims, signer, {
|
44
|
+
'kid' => 'my ec key'
|
45
45
|
})
|
46
46
|
```
|
47
47
|
|
@@ -51,9 +51,9 @@ Decoding and validating example:
|
|
51
51
|
require 'openssl'
|
52
52
|
require 'sandal'
|
53
53
|
|
54
|
-
claims = Sandal.decode_token(
|
55
|
-
if header['kid'] == 'my key
|
56
|
-
key = OpenSSL::PKey::EC.new(File.read('/path/to/
|
54
|
+
claims = Sandal.decode_token(jws_token) do |header|
|
55
|
+
if header['kid'] == 'my ec key'
|
56
|
+
key = OpenSSL::PKey::EC.new(File.read('/path/to/ec_public_key.pem'))
|
57
57
|
Sandal::Sig::ES256.new(key)
|
58
58
|
end
|
59
59
|
end
|
@@ -61,12 +61,55 @@ end
|
|
61
61
|
|
62
62
|
Keys for these examples can be generated by executing:
|
63
63
|
|
64
|
-
$ openssl ecparam -out
|
65
|
-
$ openssl ec -out
|
64
|
+
$ openssl ecparam -out ec_private_key.pem -name prime256v1 -genkey
|
65
|
+
$ openssl ec -out ec_public_key.pem -in ec_private_key.pem -pubout
|
66
66
|
|
67
67
|
## Encrypted Tokens
|
68
68
|
|
69
|
-
|
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
|
+
All the JWA encryption methods are supported:
|
72
|
+
|
73
|
+
- A128CBC+HS256, A256CBC+HS512
|
74
|
+
- A128GCM, A256GCM (note: requires ruby 2.0.0 or later)
|
75
|
+
|
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:
|
77
|
+
|
78
|
+
- RSA1_5
|
79
|
+
- RSA-OAEP
|
80
|
+
- direct
|
81
|
+
|
82
|
+
Encrypting example (assumes use of the jws_token from the signing examples, as typically JWE tokens will be used to wrap JWS tokens):
|
83
|
+
|
84
|
+
```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)
|
87
|
+
encrypter = Sandal::Enc::A128GCM.new(alg)
|
88
|
+
jwe_token = Sandal.encrypt_token(jws_token, encrypter, {
|
89
|
+
'kid': 'your rsa key',
|
90
|
+
'cty': 'JWT'
|
91
|
+
})
|
92
|
+
```
|
93
|
+
|
94
|
+
Decrypting example:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
require 'openssl'
|
98
|
+
require 'sandal'
|
99
|
+
|
100
|
+
jws_token = Sandal.decrypt_token(jwe_token) do |header|
|
101
|
+
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)
|
104
|
+
Sandal::Enc::A128GCM.new(alg)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
Keys for these examples can be generated by executing:
|
110
|
+
|
111
|
+
$ openssl genrsa -out rsa_private_key.pem 2048
|
112
|
+
$ openssl rsa -out rsa_public_key.pem -in rsa_private_key.pem -pubout
|
70
113
|
|
71
114
|
## Validation Options
|
72
115
|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'sandal/util'
|
3
|
+
|
4
|
+
module Sandal
|
5
|
+
module Enc
|
6
|
+
|
7
|
+
# Base implementation of the AES/CBC+HMAC-SHA family of encryption algorithms.
|
8
|
+
class ACBC_HS
|
9
|
+
|
10
|
+
# The JWA name of the encryption.
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# The JWA algorithm used to encrypt the content master key.
|
14
|
+
attr_reader :alg
|
15
|
+
|
16
|
+
# Creates a new instance; it's probably easier to use one of the subclass constructors.
|
17
|
+
#
|
18
|
+
# @param aes_size [Integer] The size of the AES algorithm.
|
19
|
+
# @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.
|
21
|
+
def initialize(aes_size, sha_size, alg)
|
22
|
+
@aes_size = aes_size
|
23
|
+
@sha_size = sha_size
|
24
|
+
@name = "A#{aes_size}CBC+HS#{@sha_size}"
|
25
|
+
@cipher_name = "aes-#{aes_size}-cbc"
|
26
|
+
@alg = alg
|
27
|
+
@digest = OpenSSL::Digest.new("sha#{@sha_size}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def encrypt(header, payload)
|
31
|
+
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)
|
34
|
+
|
35
|
+
cipher.key = derive_encryption_key(content_master_key)
|
36
|
+
iv = cipher.random_iv
|
37
|
+
ciphertext = cipher.update(payload) + cipher.final
|
38
|
+
|
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
|
+
|
44
|
+
secured_input << '.' << Sandal::Util.base64_encode(integrity_value)
|
45
|
+
end
|
46
|
+
|
47
|
+
def decrypt(parts, decoded_parts)
|
48
|
+
content_master_key = @alg.decrypt_cmk(decoded_parts[1])
|
49
|
+
|
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
|
53
|
+
|
54
|
+
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
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Computes the integrity value.
|
63
|
+
def compute_integrity_value(content_integrity_key, secured_input)
|
64
|
+
OpenSSL::HMAC.digest(@digest, content_integrity_key, secured_input)
|
65
|
+
end
|
66
|
+
|
67
|
+
# 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)
|
70
|
+
end
|
71
|
+
|
72
|
+
# 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)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Derives content keys using the Concat KDF.
|
78
|
+
def derive_content_key(label, content_master_key, size)
|
79
|
+
hash_input = [1].pack('N')
|
80
|
+
hash_input << content_master_key
|
81
|
+
hash_input << [size].pack('N')
|
82
|
+
hash_input << @name.encode('utf-8')
|
83
|
+
hash_input << [0].pack('N')
|
84
|
+
hash_input << [0].pack('N')
|
85
|
+
hash_input << label.encode('us-ascii')
|
86
|
+
hash = @digest.digest(hash_input)
|
87
|
+
hash[0..((size / 8) - 1)]
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
# The AES-128-CBC+HMAC-SHA256 encryption algorithm.
|
93
|
+
class A128CBC_HS256 < Sandal::Enc::ACBC_HS
|
94
|
+
def initialize(key)
|
95
|
+
super(128, 256, key)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# The AES-256-CBC+HMAC-SHA512 encryption algorithm.
|
100
|
+
class A256CBC_HS512 < Sandal::Enc::ACBC_HS
|
101
|
+
def initialize(key)
|
102
|
+
super(256, 512, key)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'sandal/util'
|
3
|
+
|
4
|
+
module Sandal
|
5
|
+
module Enc
|
6
|
+
|
7
|
+
# Base implementation of the AES/GCM family of encryption algorithms.
|
8
|
+
class AGCM
|
9
|
+
|
10
|
+
# The JWA name of the encryption.
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# The JWA algorithm used to encrypt the content master key.
|
14
|
+
attr_reader :alg
|
15
|
+
|
16
|
+
def initialize(aes_size, alg)
|
17
|
+
@aes_size = aes_size
|
18
|
+
@name = "A#{aes_size}GCM"
|
19
|
+
@cipher_name = "aes-#{aes_size}-gcm"
|
20
|
+
@alg = alg
|
21
|
+
end
|
22
|
+
|
23
|
+
def encrypt(header, payload)
|
24
|
+
cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
|
25
|
+
content_master_key = @alg.respond_to?(:cmk) ? @alg.cmk : cipher.random_key
|
26
|
+
encrypted_key = @alg.encrypt_cmk(content_master_key)
|
27
|
+
|
28
|
+
cipher.key = content_master_key
|
29
|
+
iv = cipher.random_iv
|
30
|
+
|
31
|
+
auth_parts = [MultiJson.dump(header), encrypted_key, iv]
|
32
|
+
auth_data = auth_parts.map { |part| Sandal::Util.base64_encode(part) }.join('.')
|
33
|
+
cipher.auth_data = auth_data
|
34
|
+
|
35
|
+
ciphertext = cipher.update(payload) + cipher.final
|
36
|
+
remainder = [ciphertext, cipher.auth_tag].map { |part| Sandal::Util.base64_encode(part) }.join('.')
|
37
|
+
[auth_data, remainder].join('.')
|
38
|
+
end
|
39
|
+
|
40
|
+
def decrypt(parts, decoded_parts)
|
41
|
+
cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
|
42
|
+
cipher.key = @alg.decrypt_cmk(decoded_parts[1])
|
43
|
+
cipher.iv = decoded_parts[2]
|
44
|
+
cipher.auth_tag = decoded_parts[4]
|
45
|
+
cipher.auth_data = parts.take(3).join('.')
|
46
|
+
cipher.update(decoded_parts[3]) + cipher.final
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# The AES-128-GCM encryption algorithm.
|
52
|
+
class A128GCM < Sandal::Enc::AGCM
|
53
|
+
def initialize(key)
|
54
|
+
super(128, key)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# The AES-256-GCM encryption algorithm.
|
59
|
+
class A256GCM < Sandal::Enc::AGCM
|
60
|
+
def initialize(key)
|
61
|
+
super(256, key)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Sandal
|
4
|
+
module Enc
|
5
|
+
module Alg
|
6
|
+
|
7
|
+
# The direct ("dir") key encryption mechanism, which uses a pre-shared content master key.
|
8
|
+
class Direct
|
9
|
+
|
10
|
+
# @return [String] The JWA name of the algorithm.
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# @return [String] The pre-shared content master key key.
|
14
|
+
attr_reader :cmk
|
15
|
+
|
16
|
+
# Creates a new instance.
|
17
|
+
#
|
18
|
+
# @param cmk [String] The pre-shared content master key.
|
19
|
+
def initialize(cmk)
|
20
|
+
@name = 'dir'
|
21
|
+
@cmk = cmk
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns an empty string as the content master key is not included in the JWE token.
|
25
|
+
#
|
26
|
+
# @param cmk [String] This parameter is ignored.
|
27
|
+
# @return [String] An empty string.
|
28
|
+
def encrypt_cmk(cmk)
|
29
|
+
''
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the pre-shared content master key.
|
33
|
+
#
|
34
|
+
# @param encrypted_cmk [String] The encrypted content master key.
|
35
|
+
# @return [String] The pre-shared content master key.
|
36
|
+
# @raise [Sandal::TokenError] encrypted_cmk is not nil or empty.
|
37
|
+
def decrypt_cmk(encrypted_cmk)
|
38
|
+
unless encrypted_cmk.nil? || encrypted_cmk.empty?
|
39
|
+
raise Sandal::TokenError, 'The token should not include an encrypted content master key.'
|
40
|
+
end
|
41
|
+
@cmk
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Sandal
|
4
|
+
module Enc
|
5
|
+
module Alg
|
6
|
+
|
7
|
+
# The RSAES-PKCS1-V1_5 key encryption mechanism.
|
8
|
+
class RSA1_5
|
9
|
+
|
10
|
+
# @return [String] The JWA name of the algorithm.
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# Creates a new instance.
|
14
|
+
#
|
15
|
+
# @param key [OpenSSL::PKey::RSA] The RSA public key used to protect the content master key.
|
16
|
+
def initialize(key)
|
17
|
+
@name = 'RSA1_5'
|
18
|
+
@key = key
|
19
|
+
end
|
20
|
+
|
21
|
+
# Encrypts the content master key.
|
22
|
+
#
|
23
|
+
# @param cmk [String] The content master key.
|
24
|
+
# @return [String] The encrypted content master key.
|
25
|
+
def encrypt_cmk(cmk)
|
26
|
+
@key.public_encrypt(cmk)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Decrypts the content master key.
|
30
|
+
#
|
31
|
+
# @param encrypted_cmk [String] The encrypted content master key.
|
32
|
+
# @return [String] The pre-shared content master key.
|
33
|
+
# @raise [Sandal::TokenError] The content master key cannot be decrypted.
|
34
|
+
def decrypt_cmk(encrypted_cmk)
|
35
|
+
@key.private_decrypt(encrypted_cmk)
|
36
|
+
rescue
|
37
|
+
raise Sandal::TokenError, 'Failed to decrypt the content master key.'
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Sandal
|
4
|
+
module Enc
|
5
|
+
module Alg
|
6
|
+
|
7
|
+
# The RSAES with OAEP key encryption mechanism.
|
8
|
+
class RSA_OAEP
|
9
|
+
|
10
|
+
# @return [String] The JWA name of the algorithm.
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# Creates a new instance.
|
14
|
+
#
|
15
|
+
# @param key [OpenSSL::PKey::RSA] The RSA public key used to protect the content master key.
|
16
|
+
def initialize(key)
|
17
|
+
@name = 'RSA-OAEP'
|
18
|
+
@key = key
|
19
|
+
@padding = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
|
20
|
+
end
|
21
|
+
|
22
|
+
# Encrypts the content master key.
|
23
|
+
#
|
24
|
+
# @param cmk [String] The content master key.
|
25
|
+
# @return [String] The encrypted content master key.
|
26
|
+
def encrypt_cmk(cmk)
|
27
|
+
@key.public_encrypt(cmk, @padding)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Decrypts the content master key.
|
31
|
+
#
|
32
|
+
# @param encrypted_cmk [String] The encrypted content master key.
|
33
|
+
# @return [String] The pre-shared content master key.
|
34
|
+
# @raise [Sandal::TokenError] The content master key cannot be decrypted.
|
35
|
+
def decrypt_cmk(encrypted_cmk)
|
36
|
+
@key.private_decrypt(encrypted_cmk, @padding)
|
37
|
+
rescue
|
38
|
+
raise Sandal::TokenError, 'Failed to decrypt the content master key.'
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/sandal/enc.rb
CHANGED
@@ -1,25 +1,9 @@
|
|
1
1
|
module Sandal
|
2
|
-
#
|
2
|
+
# Contains encryption (JWE) functionality.
|
3
3
|
module Enc
|
4
|
-
|
5
|
-
# The JWA name of the encryption.
|
6
|
-
attr_reader :name
|
7
|
-
|
8
|
-
# The JWA name of the algorithm.
|
9
|
-
attr_reader :alg_name
|
10
|
-
|
11
|
-
# Encrypts a header and payload, and returns an encrypted token.
|
12
|
-
def encrypt(header, payload)
|
13
|
-
raise NotImplementedError, "#{@name}.encrypt is not implemented."
|
14
|
-
end
|
15
|
-
|
16
|
-
# Decrypts a token.
|
17
|
-
def decrypt(encrypted_key, iv, ciphertext, secured_input, integrity_value)
|
18
|
-
raise NotImplementedError, "#{@name}.decrypt is not implemented."
|
19
|
-
end
|
20
|
-
|
21
4
|
end
|
22
5
|
end
|
23
6
|
|
24
|
-
require 'sandal/enc/
|
25
|
-
require 'sandal/enc/
|
7
|
+
require 'sandal/enc/acbc_hs'
|
8
|
+
require 'sandal/enc/agcm' unless RUBY_VERSION < '2.0.0'
|
9
|
+
require 'sandal/enc/alg'
|
data/lib/sandal/sig.rb
CHANGED
data/lib/sandal/version.rb
CHANGED
data/lib/sandal.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
$:.unshift('.')
|
2
|
-
|
3
|
-
require 'base64'
|
4
1
|
require 'multi_json'
|
5
2
|
require 'openssl'
|
3
|
+
require 'sandal/version'
|
6
4
|
require 'sandal/claims'
|
5
|
+
require 'sandal/enc'
|
6
|
+
require 'sandal/sig'
|
7
7
|
require 'sandal/util'
|
8
|
-
|
8
|
+
|
9
9
|
|
10
10
|
# A library for creating and reading JSON Web Tokens (JWT).
|
11
11
|
module Sandal
|
@@ -23,7 +23,6 @@ module Sandal
|
|
23
23
|
# valid_aud:: A list of valid audiences, if audience validation is required.
|
24
24
|
# validate_exp:: Whether the expiry date of the token is validated.
|
25
25
|
# validate_nbf:: Whether the not-before date of the token is validated.
|
26
|
-
# validate_integrity:: Whether the integrity value of encrypted (JWE) tokens is validated.
|
27
26
|
# validate_signature:: Whether the signature of signed (JWS) tokens is validated.
|
28
27
|
DEFAULT_OPTIONS = {
|
29
28
|
max_clock_skew: 300,
|
@@ -31,7 +30,6 @@ module Sandal
|
|
31
30
|
valid_aud: [],
|
32
31
|
validate_exp: true,
|
33
32
|
validate_nbf: true,
|
34
|
-
validate_integrity: true,
|
35
33
|
validate_signature: true
|
36
34
|
}
|
37
35
|
|
@@ -43,7 +41,7 @@ module Sandal
|
|
43
41
|
DEFAULT_OPTIONS.merge!(defaults)
|
44
42
|
end
|
45
43
|
|
46
|
-
# Creates a signed JSON Web Token.
|
44
|
+
# Creates a signed JSON Web Token (JWS).
|
47
45
|
#
|
48
46
|
# @param payload [String/Hash] The payload of the token. Hashes will be encoded as JSON.
|
49
47
|
# @param signer [#name,#sign] The token signer, which may be nil for an unsigned token.
|
@@ -55,34 +53,16 @@ module Sandal
|
|
55
53
|
header = {}
|
56
54
|
header['alg'] = signer.name if signer.name != Sandal::Sig::None.instance.name
|
57
55
|
header = header_fields.merge(header) if header_fields
|
56
|
+
header = MultiJson.dump(header)
|
58
57
|
|
59
58
|
payload = MultiJson.dump(payload) unless payload.is_a?(String)
|
60
59
|
|
61
|
-
|
62
|
-
encoded_payload = Sandal::Util.base64_encode(payload)
|
63
|
-
secured_input = [encoded_header, encoded_payload].join('.')
|
64
|
-
|
60
|
+
secured_input = [header, payload].map { |part| Sandal::Util.base64_encode(part) }.join('.')
|
65
61
|
signature = signer.sign(secured_input)
|
66
|
-
|
67
|
-
[secured_input, encoded_signature].join('.')
|
68
|
-
end
|
69
|
-
|
70
|
-
# Creates an encrypted JSON Web Token.
|
71
|
-
#
|
72
|
-
# @param payload [String] The payload of the token.
|
73
|
-
# @param encrypter [Sandal::Enc] The token encrypter.
|
74
|
-
# @param header_fields [Hash] Header fields for the token (note: do not include 'alg' or 'enc').
|
75
|
-
# @return [String] An encrypted JSON Web Token.
|
76
|
-
def self.encrypt_token(payload, encrypter, header_fields = nil)
|
77
|
-
header = {}
|
78
|
-
header['enc'] = encrypter.name
|
79
|
-
header['alg'] = encrypter.alg_name
|
80
|
-
header = header_fields.merge(header) if header_fields
|
81
|
-
|
82
|
-
encrypter.encrypt(header, payload)
|
62
|
+
[secured_input, Sandal::Util.base64_encode(signature)].join('.')
|
83
63
|
end
|
84
64
|
|
85
|
-
# Decodes and validates a JSON Web Token.
|
65
|
+
# Decodes and validates a signed JSON Web Token (JWS).
|
86
66
|
#
|
87
67
|
# The block is called with the token header as the first parameter, and should return the appropriate
|
88
68
|
# {Sandal::Sig} to validate the signature. It can optionally have a second options parameter which can
|
@@ -96,7 +76,7 @@ module Sandal
|
|
96
76
|
# @raise [Sandal::TokenError] The token format is invalid, or validation of the token failed.
|
97
77
|
def self.decode_token(token)
|
98
78
|
parts = token.split('.')
|
99
|
-
header, payload, signature =
|
79
|
+
header, payload, signature = decode_jws_token_parts(parts)
|
100
80
|
|
101
81
|
options = DEFAULT_OPTIONS.clone
|
102
82
|
validator = yield header, options if block_given?
|
@@ -110,45 +90,72 @@ module Sandal
|
|
110
90
|
parse_and_validate(payload, header['cty'], options)
|
111
91
|
end
|
112
92
|
|
113
|
-
#
|
93
|
+
# Creates an encrypted JSON Web Token (JWE).
|
114
94
|
#
|
115
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
95
|
+
# @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').
|
98
|
+
# @return [String] An encrypted JSON Web Token.
|
99
|
+
def self.encrypt_token(payload, encrypter, header_fields = nil)
|
100
|
+
header = {}
|
101
|
+
header['enc'] = encrypter.name
|
102
|
+
header['alg'] = encrypter.alg.name
|
103
|
+
header = header_fields.merge(header) if header_fields
|
104
|
+
|
105
|
+
encrypter.encrypt(header, payload)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Decrypts and validates an encrypted JSON Web Token (JWE).
|
109
|
+
#
|
110
|
+
# @param token [String] The encrypted JSON Web Token.
|
111
|
+
# @yieldparam header [Hash] The JWT header values.
|
112
|
+
# @yieldparam options [Hash] (Optional) A hash that can be used to override the default options.
|
113
|
+
# @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.
|
116
|
+
def self.decrypt_token(token)
|
117
|
+
parts = token.split('.')
|
118
|
+
decoded_parts = decode_jwe_token_parts(parts)
|
119
|
+
header = decoded_parts[0]
|
120
|
+
|
121
|
+
options = DEFAULT_OPTIONS.clone
|
122
|
+
decrypter = yield header, options if block_given?
|
128
123
|
|
129
|
-
|
130
|
-
|
131
|
-
enc.decrypt(encrypted_key, iv, ciphertext, parts.take(4).join('.'), integrity_value)
|
124
|
+
payload = decrypter.decrypt(parts, decoded_parts)
|
125
|
+
parse_and_validate(payload, header['cty'], options)
|
132
126
|
end
|
133
127
|
|
134
|
-
|
128
|
+
private
|
135
129
|
|
136
130
|
# Decodes the parts of a JWS token.
|
137
|
-
def self.
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
131
|
+
def self.decode_jws_token_parts(parts)
|
132
|
+
parts = decode_token_parts(parts)
|
133
|
+
parts << '' if parts.length == 2
|
134
|
+
raise TokenError, 'Invalid token format.' unless parts.length == 3
|
135
|
+
parts
|
136
|
+
end
|
137
|
+
|
138
|
+
# Decodes the parts of a JWE token.
|
139
|
+
def self.decode_jwe_token_parts(parts)
|
140
|
+
parts = decode_token_parts(parts)
|
141
|
+
raise TokenError, 'Invalid token format.' unless parts.length == 5
|
142
|
+
parts
|
143
|
+
end
|
144
|
+
|
145
|
+
# Decodes the parts of a token.
|
146
|
+
def self.decode_token_parts(parts)
|
147
|
+
parts = parts.map { |part| Sandal::Util.base64_decode(part) }
|
148
|
+
parts[0] = MultiJson.load(parts[0])
|
149
|
+
parts
|
150
|
+
rescue
|
151
|
+
raise TokenError, 'Invalid token encoding.'
|
147
152
|
end
|
148
153
|
|
149
154
|
# Parses the content of a token and validates the claims if is a JSON claim set.
|
150
155
|
def self.parse_and_validate(payload, content_type, options)
|
151
|
-
|
156
|
+
return payload if content_type == 'JWT'
|
157
|
+
|
158
|
+
claims = MultiJson.load(payload) rescue nil
|
152
159
|
if claims
|
153
160
|
claims.extend(Sandal::Claims).validate_claims(options)
|
154
161
|
else
|
@@ -156,7 +163,4 @@ module Sandal
|
|
156
163
|
end
|
157
164
|
end
|
158
165
|
|
159
|
-
end
|
160
|
-
|
161
|
-
require 'sandal/enc'
|
162
|
-
require 'sandal/sig'
|
166
|
+
end
|
data/spec/helper.rb
CHANGED
@@ -2,7 +2,14 @@ require 'coveralls'
|
|
2
2
|
Coveralls.wear!
|
3
3
|
|
4
4
|
require 'rspec'
|
5
|
-
require "#{File.dirname(__FILE__)}/../lib/sandal.rb"
|
6
|
-
|
7
5
|
RSpec.configure do |c|
|
8
|
-
|
6
|
+
c.treat_symbols_as_metadata_keys_with_true_values = true
|
7
|
+
c.filter_run_excluding :jruby_incompatible if RUBY_PLATFORM == 'java'
|
8
|
+
end
|
9
|
+
|
10
|
+
def make_bn(arr)
|
11
|
+
hex_str = arr.pack('C*').unpack('H*')[0]
|
12
|
+
OpenSSL::BN.new(hex_str, 16)
|
13
|
+
end
|
14
|
+
|
15
|
+
require "#{File.dirname(__FILE__)}/../lib/sandal.rb"
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'openssl'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
# TODO: These tests are really for the Sandal module rather than just the algorithm -- move them!
|
6
|
+
|
7
|
+
describe Sandal::Enc::A128CBC_HS256 do
|
8
|
+
|
9
|
+
# these tests don't run with jruby as it errors when you try and set rsa.d parameter directly
|
10
|
+
context 'using the example RSA key from JWE section A.2', :jruby_incompatible do
|
11
|
+
|
12
|
+
before :all do
|
13
|
+
@rsa = OpenSSL::PKey::RSA.new(2048)
|
14
|
+
@rsa.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])
|
15
|
+
@rsa.e = make_bn([1, 0, 1])
|
16
|
+
@rsa.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])
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'can decrypt the example token' do
|
20
|
+
token = 'eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDK0hTMjU2In0.ZmnlqWgjXyqwjr7cXHys8F79anIUI6J2UWdAyRQEcGBU-KPHsePM910_RoTDGu1IW40Dn0dvcdVEjpJcPPNIbzWcMxDi131Ejeg-b8ViW5YX5oRdYdiR4gMSDDB3mbkInMNUFT-PK5CuZRnHB2rUK5fhPuF6XFqLLZCG5Q_rJm6Evex-XLcNQAJNa1-6CIU12Wj3mPExxw9vbnsQDU7B4BfmhdyiflLA7Ae5ZGoVRl3A__yLPXxRjHFhpOeDp_adx8NyejF5cz9yDKULugNsDMdlHeJQOMGVLYaSZt3KP6aWNSqFA1PHDg-10ceuTEtq_vPE4-Gtev4N4K4Eudlj4Q.AxY8DCtDaGlsbGljb3RoZQ.Rxsjg6PIExcmGSF7LnSEkDqWIKfAw1wZz2XpabV5PwQsolKwEauWYZNE9Q1hZJEZ.8LXqMd0JLGsxMaB5uoNaMpg7uUW_p40RlaZHCwMIyzk'
|
21
|
+
payload = Sandal.decrypt_token(token) do |header|
|
22
|
+
alg = Sandal::Enc::Alg::RSA1_5.new(@rsa)
|
23
|
+
encrypter = Sandal::Enc::A128CBC_HS256.new(alg)
|
24
|
+
end
|
25
|
+
payload.should == 'No matter where you go, there you are.'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'raises a token error when the integrity value is changed' do
|
29
|
+
token = 'eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDK0hTMjU2In0.ZmnlqWgjXyqwjr7cXHys8F79anIUI6J2UWdAyRQEcGBU-KPHsePM910_RoTDGu1IW40Dn0dvcdVEjpJcPPNIbzWcMxDi131Ejeg-b8ViW5YX5oRdYdiR4gMSDDB3mbkInMNUFT-PK5CuZRnHB2rUK5fhPuF6XFqLLZCG5Q_rJm6Evex-XLcNQAJNa1-6CIU12Wj3mPExxw9vbnsQDU7B4BfmhdyiflLA7Ae5ZGoVRl3A__yLPXxRjHFhpOeDp_adx8NyejF5cz9yDKULugNsDMdlHeJQOMGVLYaSZt3KP6aWNSqFA1PHDg-10ceuTEtq_vPE4-Gtev4N4K4Eudlj4Q.AxY8DCtDaGlsbGljb3RoZQ.Rxsjg6PIExcmGSF7LnSEkDqWIKfAw1wZz2XpabV5PwQsolKwEauWYZNE9Q1hZJEZ.7V5ZDko0v_mf2PAc4JMiUg'
|
30
|
+
expect { Sandal.decrypt_token(token) do |header|
|
31
|
+
alg = Sandal::Enc::Alg::RSA1_5.new(@rsa)
|
32
|
+
encrypter = Sandal::Enc::A128CBC_HS256.new(alg)
|
33
|
+
end }.to raise_error Sandal::TokenError, 'Invalid integrity value.'
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'raises a token error when the RSA keys JWE section A.2 are changed' do
|
39
|
+
token = 'eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDK0hTMjU2In0.ZmnlqWgjXyqwjr7cXHys8F79anIUI6J2UWdAyRQEcGBU-KPHsePM910_RoTDGu1IW40Dn0dvcdVEjpJcPPNIbzWcMxDi131Ejeg-b8ViW5YX5oRdYdiR4gMSDDB3mbkInMNUFT-PK5CuZRnHB2rUK5fhPuF6XFqLLZCG5Q_rJm6Evex-XLcNQAJNa1-6CIU12Wj3mPExxw9vbnsQDU7B4BfmhdyiflLA7Ae5ZGoVRl3A__yLPXxRjHFhpOeDp_adx8NyejF5cz9yDKULugNsDMdlHeJQOMGVLYaSZt3KP6aWNSqFA1PHDg-10ceuTEtq_vPE4-Gtev4N4K4Eudlj4Q.AxY8DCtDaGlsbGljb3RoZQ.Rxsjg6PIExcmGSF7LnSEkDqWIKfAw1wZz2XpabV5PwQsolKwEauWYZNE9Q1hZJEZ.8LXqMd0JLGsxMaB5uoNaMpg7uUW_p40RlaZHCwMIyzk'
|
40
|
+
expect { Sandal.decrypt_token(token) do |header|
|
41
|
+
rsa = OpenSSL::PKey::RSA.new(2048)
|
42
|
+
alg = Sandal::Enc::Alg::RSA1_5.new(rsa)
|
43
|
+
encrypter = Sandal::Enc::A128CBC_HS256.new(alg)
|
44
|
+
end }.to raise_error Sandal::TokenError, 'Failed to decrypt the content master key.'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'can encrypt and decrypt tokens with the "dir" algorithm' do
|
48
|
+
payload = 'Some text to encrypt'
|
49
|
+
content_master_key = SecureRandom.random_bytes(16)
|
50
|
+
|
51
|
+
encrypter = Sandal::Enc::A128CBC_HS256.new(Sandal::Enc::Alg::Direct.new(content_master_key))
|
52
|
+
token = Sandal.encrypt_token(payload, encrypter)
|
53
|
+
|
54
|
+
output = Sandal.decrypt_token(token) { encrypter }
|
55
|
+
output.should == payload
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'can encrypt and decrypt tokens with the RSA1_5 algorithm' do
|
59
|
+
payload = 'Some other text to encrypt'
|
60
|
+
rsa = OpenSSL::PKey::RSA.new(2048)
|
61
|
+
|
62
|
+
encrypter = Sandal::Enc::A128CBC_HS256.new(Sandal::Enc::Alg::RSA1_5.new(rsa.public_key))
|
63
|
+
token = Sandal.encrypt_token(payload, encrypter)
|
64
|
+
|
65
|
+
output = Sandal.decrypt_token(token) do
|
66
|
+
Sandal::Enc::A128CBC_HS256.new(Sandal::Enc::Alg::RSA1_5.new(rsa))
|
67
|
+
end
|
68
|
+
output.should == payload
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'can encrypt and decrypt tokens with the RSA-OAEP algorithm' do
|
72
|
+
payload = 'Some more text to encrypt'
|
73
|
+
rsa = OpenSSL::PKey::RSA.new(2048)
|
74
|
+
|
75
|
+
encrypter = Sandal::Enc::A128CBC_HS256.new(Sandal::Enc::Alg::RSA_OAEP.new(rsa.public_key))
|
76
|
+
token = Sandal.encrypt_token(payload, encrypter)
|
77
|
+
|
78
|
+
output = Sandal.decrypt_token(token) do
|
79
|
+
Sandal::Enc::A128CBC_HS256.new(Sandal::Enc::Alg::RSA_OAEP.new(rsa))
|
80
|
+
end
|
81
|
+
output.should == payload
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'openssl'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
# TODO: These tests are really for the Sandal module rather than just the algorithm -- move them!
|
6
|
+
|
7
|
+
if defined? Sandal::Enc::A128GCM
|
8
|
+
|
9
|
+
describe Sandal::Enc::A128GCM do
|
10
|
+
|
11
|
+
it 'can encrypt and decrypt tokens with the RSA-OAEP algorithm' do
|
12
|
+
payload = 'Some more text to encrypt'
|
13
|
+
rsa = OpenSSL::PKey::RSA.new(2048)
|
14
|
+
|
15
|
+
encrypter = Sandal::Enc::A128GCM.new(Sandal::Enc::Alg::RSA_OAEP.new(rsa.public_key))
|
16
|
+
token = Sandal.encrypt_token(payload, encrypter)
|
17
|
+
|
18
|
+
output = Sandal.decrypt_token(token) do
|
19
|
+
Sandal::Enc::A128GCM.new(Sandal::Enc::Alg::RSA_OAEP.new(rsa))
|
20
|
+
end
|
21
|
+
output.should == payload
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/spec/sandal/sig/es_spec.rb
CHANGED
@@ -2,12 +2,7 @@ require 'helper'
|
|
2
2
|
require 'openssl'
|
3
3
|
|
4
4
|
# EC isn't implemented in jruby-openssl at the moment
|
5
|
-
if defined?
|
6
|
-
|
7
|
-
def make_bn(arr)
|
8
|
-
hex_str = arr.pack('C*').unpack('H*')[0]
|
9
|
-
OpenSSL::BN.new(hex_str, 16)
|
10
|
-
end
|
5
|
+
if defined? Sandal::Sig::ES
|
11
6
|
|
12
7
|
def make_point(group, x, y)
|
13
8
|
def pad(c)
|
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.2.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-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multi_json
|
@@ -28,84 +28,84 @@ dependencies:
|
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - '>='
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '1.3'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - '>='
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.3'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - '>='
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '10.0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - '>='
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '10.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rspec
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '2.13'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - '>='
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '2.13'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: coveralls
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - '>='
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0.6'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - '>='
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0.6'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: yard
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- -
|
87
|
+
- - '>='
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: '0.8'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- -
|
94
|
+
- - '>='
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0.8'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: redcarpet
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- -
|
101
|
+
- - '>='
|
102
102
|
- !ruby/object:Gem::Version
|
103
103
|
version: '2.2'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- -
|
108
|
+
- - '>='
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '2.2'
|
111
111
|
description: A ruby library for creating and reading JSON Web Tokens (JWT), supporting
|
@@ -130,8 +130,12 @@ files:
|
|
130
130
|
- lib/sandal.rb
|
131
131
|
- lib/sandal/claims.rb
|
132
132
|
- lib/sandal/enc.rb
|
133
|
-
- lib/sandal/enc/
|
134
|
-
- lib/sandal/enc/
|
133
|
+
- lib/sandal/enc/acbc_hs.rb
|
134
|
+
- lib/sandal/enc/agcm.rb
|
135
|
+
- lib/sandal/enc/alg.rb
|
136
|
+
- lib/sandal/enc/alg/direct.rb
|
137
|
+
- lib/sandal/enc/alg/rsa1_5.rb
|
138
|
+
- lib/sandal/enc/alg/rsa_oaep.rb
|
135
139
|
- lib/sandal/sig.rb
|
136
140
|
- lib/sandal/sig/es.rb
|
137
141
|
- lib/sandal/sig/hs.rb
|
@@ -140,6 +144,8 @@ files:
|
|
140
144
|
- lib/sandal/version.rb
|
141
145
|
- sandal.gemspec
|
142
146
|
- spec/helper.rb
|
147
|
+
- spec/sandal/enc/a128cbc_hs256_spec.rb
|
148
|
+
- spec/sandal/enc/a128gcm_spec.rb
|
143
149
|
- spec/sandal/sig/es_spec.rb
|
144
150
|
- spec/sandal/sig/hs_spec.rb
|
145
151
|
- spec/sandal/sig/rs_spec.rb
|
@@ -155,12 +161,12 @@ require_paths:
|
|
155
161
|
- lib
|
156
162
|
required_ruby_version: !ruby/object:Gem::Requirement
|
157
163
|
requirements:
|
158
|
-
- -
|
164
|
+
- - '>='
|
159
165
|
- !ruby/object:Gem::Version
|
160
166
|
version: '0'
|
161
167
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
162
168
|
requirements:
|
163
|
-
- -
|
169
|
+
- - '>='
|
164
170
|
- !ruby/object:Gem::Version
|
165
171
|
version: '0'
|
166
172
|
requirements:
|
@@ -172,6 +178,8 @@ specification_version: 4
|
|
172
178
|
summary: A JSON Web Token (JWT) library.
|
173
179
|
test_files:
|
174
180
|
- spec/helper.rb
|
181
|
+
- spec/sandal/enc/a128cbc_hs256_spec.rb
|
182
|
+
- spec/sandal/enc/a128gcm_spec.rb
|
175
183
|
- spec/sandal/sig/es_spec.rb
|
176
184
|
- spec/sandal/sig/hs_spec.rb
|
177
185
|
- spec/sandal/sig/rs_spec.rb
|
data/lib/sandal/enc/aescbc.rb
DELETED
@@ -1,91 +0,0 @@
|
|
1
|
-
require 'openssl'
|
2
|
-
require 'sandal/util'
|
3
|
-
|
4
|
-
module Sandal
|
5
|
-
module Enc
|
6
|
-
|
7
|
-
# Base implementation of the AES/CBC family of encryption algorithms.
|
8
|
-
class AESCBC
|
9
|
-
include Sandal::Enc
|
10
|
-
|
11
|
-
def initialize(aes_size, key)
|
12
|
-
raise ArgumentError, 'A key is required.' unless key
|
13
|
-
@aes_size = aes_size
|
14
|
-
@sha_size = aes_size * 2 # TODO: Any smarter way to do this?
|
15
|
-
@name = "A#{aes_size}CBC+HS#{@sha_size}"
|
16
|
-
@alg_name = "RSA1_5" # TODO: From key?
|
17
|
-
@cipher_name = "aes-#{aes_size}-cbc"
|
18
|
-
@key = key
|
19
|
-
@digest = OpenSSL::Digest.new("sha#{@sha_size}")
|
20
|
-
end
|
21
|
-
|
22
|
-
def encrypt(header, payload)
|
23
|
-
cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
|
24
|
-
content_master_key = cipher.random_key # TODO: Check with the spec if this is long enough
|
25
|
-
iv = cipher.random_iv
|
26
|
-
|
27
|
-
# TODO: Need to think about how this works with pre-shared symmetric keys - I'd originally thought
|
28
|
-
# this wouldn't be a common use case, but in cases where the recipient is also the issuer (e.g.
|
29
|
-
# an OAuth refresh token) then it would make a lot of sense.
|
30
|
-
encrypted_key = @key.public_encrypt(content_master_key)
|
31
|
-
encoded_encrypted_key = Sandal::Util.base64_encode(encrypted_key)
|
32
|
-
encoded_iv = Sandal::Util.base64_encode(iv)
|
33
|
-
|
34
|
-
cipher.key = derive_content_key('Encryption', content_master_key, @aes_size)
|
35
|
-
ciphertext = cipher.update(payload) + cipher.final
|
36
|
-
encoded_ciphertext = Sandal::Util.base64_encode(ciphertext)
|
37
|
-
|
38
|
-
encoded_header = Sandal::Util.base64_encode(MultiJson.dump(header))
|
39
|
-
secured_input = [encoded_header, encoded_encrypted_key, encoded_iv, encoded_ciphertext].join('.')
|
40
|
-
content_integrity_key = derive_content_key('Integrity', content_master_key, @sha_size)
|
41
|
-
integrity_value = OpenSSL::HMAC.digest(@digest, content_integrity_key, secured_input)
|
42
|
-
encoded_integrity_value = Sandal::Util.base64_encode(integrity_value)
|
43
|
-
|
44
|
-
[secured_input, encoded_integrity_value].join('.')
|
45
|
-
end
|
46
|
-
|
47
|
-
def decrypt(encrypted_key, iv, ciphertext, secured_input, integrity_value)
|
48
|
-
content_master_key = @key.private_decrypt(encrypted_key)
|
49
|
-
|
50
|
-
content_integrity_key = derive_content_key('Integrity', content_master_key, @sha_size)
|
51
|
-
computed_integrity_value = OpenSSL::HMAC.digest(@digest, content_integrity_key, secured_input)
|
52
|
-
raise ArgumentError, 'Invalid signature.' unless integrity_value == computed_integrity_value
|
53
|
-
|
54
|
-
cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
|
55
|
-
cipher.key = derive_content_key('Encryption', content_master_key, @aes_size)
|
56
|
-
cipher.iv = iv
|
57
|
-
cipher.update(ciphertext) + cipher.final
|
58
|
-
end
|
59
|
-
|
60
|
-
private
|
61
|
-
|
62
|
-
# Derives content keys using the Concat KDF.
|
63
|
-
def derive_content_key(label, content_master_key, size)
|
64
|
-
round_number = [1].pack('N')
|
65
|
-
output_size = [size].pack('N')
|
66
|
-
enc_bytes = @name.encode('utf-8').bytes.to_a.pack('C*')
|
67
|
-
epu = epv = [0].pack('N')
|
68
|
-
label_bytes = label.encode('us-ascii').bytes.to_a.pack('C*')
|
69
|
-
hash_input = round_number + content_master_key + output_size + enc_bytes + epu + epv + label_bytes
|
70
|
-
hash = @digest.digest(hash_input)
|
71
|
-
hash[0..((size / 8) - 1)]
|
72
|
-
end
|
73
|
-
|
74
|
-
end
|
75
|
-
|
76
|
-
# The AES-128-CBC encryption algorithm.
|
77
|
-
class AES128CBC < Sandal::Enc::AESCBC
|
78
|
-
def initialize(key)
|
79
|
-
super(128, key)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
# The AES-256-CBC encryption algorithm.
|
84
|
-
class AES256CBC < Sandal::Enc::AESCBC
|
85
|
-
def initialize(key)
|
86
|
-
super(256, key)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
end
|
91
|
-
end
|
data/lib/sandal/enc/aesgcm.rb
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
require 'openssl'
|
2
|
-
require 'sandal/util'
|
3
|
-
|
4
|
-
module Sandal
|
5
|
-
module Enc
|
6
|
-
|
7
|
-
# Base implementation of the AES/GCM family of encryption algorithms.
|
8
|
-
class AESGCM
|
9
|
-
include Sandal::Enc
|
10
|
-
|
11
|
-
def initialize(aes_size, key)
|
12
|
-
raise NotImplementedException, 'AES-CGM is not yet implemented.'
|
13
|
-
end
|
14
|
-
|
15
|
-
def encrypt(header, payload)
|
16
|
-
raise NotImplementedException, 'AES-CGM is not yet implemented.'
|
17
|
-
end
|
18
|
-
|
19
|
-
def decrypt(encrypted_key, iv, ciphertext, secured_input, integrity_value)
|
20
|
-
raise NotImplementedException, 'AES-CGM is not yet implemented.'
|
21
|
-
end
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
# The AES-128-GCM encryption algorithm.
|
26
|
-
class AES128GCM < Sandal::Enc::AESGCM
|
27
|
-
def initialize(key)
|
28
|
-
super(128, key)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# The AES-256-GCM encryption algorithm.
|
33
|
-
class AES256GCM < Sandal::Enc::AESGCM
|
34
|
-
def initialize(key)
|
35
|
-
super(256, key)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
end
|
40
|
-
end
|