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 CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- N2E4NjhhOTY5Y2JkNGU1MmU1YjE2MmIzYTY4NzUwMTg5ZTc3NTk0OA==
5
- data.tar.gz: !binary |-
6
- YjA1YTA1MzExMDg2YmMwNDllNWQyYTgxZjNmMmQ0MjJhNWNiNTM0Mw==
7
- !binary "U0hBNTEy":
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) drfat-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.
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
- The signing part of the library is a lot smaller and easier to implement than the encryption part, so I focused on that first. All the JWA signature methods are supported:
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/private_key.pem'))
41
+ key = OpenSSL::PKey::EC.new(File.read('/path/to/ec_private_key.pem'))
42
42
  signer = Sandal::Sig::ES256.new(key)
43
- token = Sandal.encode_token(claims, signer, {
44
- 'kid' => 'my key identifier'
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(token) do |header|
55
- if header['kid'] == 'my key identifier'
56
- key = OpenSSL::PKey::EC.new(File.read('/path/to/public_key.pem'))
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 private_key.pem -name prime256v1 -genkey
65
- $ openssl ec -out public_key.pem -in private_key.pem -pubout
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
- This part of the library still needs a lot of work. The current version supports the AES/CBC algorithms and RSA1_5 key protection, but expect a lot of changes here. I'd avoid it for now.
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
@@ -0,0 +1,11 @@
1
+ module Sandal
2
+ module Enc
3
+ # Contains key encryption algorithms for JWE.
4
+ module Alg
5
+ end
6
+ end
7
+ end
8
+
9
+ require 'sandal/enc/alg/direct'
10
+ require 'sandal/enc/alg/rsa1_5'
11
+ require 'sandal/enc/alg/rsa_oaep'
data/lib/sandal/enc.rb CHANGED
@@ -1,25 +1,9 @@
1
1
  module Sandal
2
- # Common encryption traits.
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/aescbc'
25
- require 'sandal/enc/aesgcm'
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
@@ -38,6 +38,6 @@ module Sandal
38
38
  end
39
39
  end
40
40
 
41
- require 'sandal/sig/es'
41
+ require 'sandal/sig/es' unless RUBY_PLATFORM == 'java'
42
42
  require 'sandal/sig/hs'
43
43
  require 'sandal/sig/rs'
@@ -1,4 +1,4 @@
1
1
  module Sandal
2
2
  # The semantic version of the library.
3
- VERSION = '0.1.1'
3
+ VERSION = '0.2.0'
4
4
  end
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
- require 'sandal/version'
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
- encoded_header = Sandal::Util.base64_encode(MultiJson.dump(header))
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
- encoded_signature = Sandal::Util.base64_encode(signature)
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 = decode_jws_parts(parts)
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
- # Decrypts an encrypted JSON Web Token.
93
+ # Creates an encrypted JSON Web Token (JWE).
114
94
  #
115
- # **NOTE: This method is likely to change, to allow more validation options**
116
- def self.decrypt_token(encrypted_token, &enc_finder)
117
- parts = encrypted_token.split('.')
118
- raise ArgumentError, 'Invalid token format.' unless parts.length == 5
119
- begin
120
- header = MultiJson.load(Sandal::Util.base64_decode(parts[0]))
121
- encrypted_key = Sandal::Util.base64_decode(parts[1])
122
- iv = Sandal::Util.base64_decode(parts[2])
123
- ciphertext = Sandal::Util.base64_decode(parts[3])
124
- integrity_value = Sandal::Util.base64_decode(parts[4])
125
- rescue
126
- raise ArgumentError, 'Invalid token encoding.'
127
- end
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
- enc = enc_finder.call(header)
130
- raise TokenError, 'No decryptor was found.' unless enc
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
- private
128
+ private
135
129
 
136
130
  # Decodes the parts of a JWS token.
137
- def self.decode_jws_parts(parts)
138
- raise TokenError, 'Invalid token format.' unless [2, 3].include?(parts.length)
139
- begin
140
- header = MultiJson.load(Sandal::Util.base64_decode(parts[0]))
141
- payload = Sandal::Util.base64_decode(parts[1])
142
- signature = if parts.length > 2 then Sandal::Util.base64_decode(parts[2]) else '' end
143
- rescue
144
- raise TokenError, 'Invalid token encoding.'
145
- end
146
- return header, payload, signature
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
- claims = MultiJson.load(payload) rescue nil unless content_type == 'JWT'
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
- end
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
@@ -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? OpenSSL::PKey::EC
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.1.1
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-01 00:00:00.000000000 Z
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/aescbc.rb
134
- - lib/sandal/enc/aesgcm.rb
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
@@ -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
@@ -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