sandal 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/sandal.rb +8 -49
- data/lib/sandal/enc.rb +3 -2
- data/lib/sandal/enc/aescbc.rb +13 -0
- data/lib/sandal/enc/aesgcm.rb +40 -0
- data/lib/sandal/sig.rb +4 -0
- data/lib/sandal/sig/es.rb +88 -0
- data/lib/sandal/version.rb +1 -1
- metadata +9 -5
- checksums.yaml +0 -15
data/lib/sandal.rb
CHANGED
@@ -16,10 +16,10 @@ module Sandal
|
|
16
16
|
if header_fields && header_fields['enc']
|
17
17
|
throw ArgumentError.new('The header cannot contain an "enc" parameter.')
|
18
18
|
end
|
19
|
-
sig ||= Sandal::Sig::None.
|
19
|
+
sig ||= Sandal::Sig::None.instance
|
20
20
|
|
21
21
|
header = {}
|
22
|
-
header['alg'] = sig.name if sig.name !=
|
22
|
+
header['alg'] = sig.name if sig.name != Sandal::Sig::None.instance.name
|
23
23
|
header = header_fields.merge(header) if header_fields
|
24
24
|
|
25
25
|
encoded_header = Sandal::Util.base64_encode(JSON.generate(header))
|
@@ -66,7 +66,7 @@ module Sandal
|
|
66
66
|
end
|
67
67
|
|
68
68
|
# Decrypts a token.
|
69
|
-
def self.decrypt_token(encrypted_token, &
|
69
|
+
def self.decrypt_token(encrypted_token, &enc_finder)
|
70
70
|
parts = encrypted_token.split('.')
|
71
71
|
throw ArgumentError.new('Invalid token format.') unless parts.length == 5
|
72
72
|
begin
|
@@ -79,50 +79,9 @@ module Sandal
|
|
79
79
|
throw ArgumentError.new('Invalid token encoding.')
|
80
80
|
end
|
81
81
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
when 'A128CBC+HS256', 'A256CBC+HS512'
|
86
|
-
aes_length = Integer(encryption[1..3])
|
87
|
-
sha_length = Integer(encryption[-3..-1])
|
88
|
-
|
89
|
-
digest = OpenSSL::Digest.new("SHA#{sha_length}")
|
90
|
-
|
91
|
-
private_key = key_finder.call(header)
|
92
|
-
throw SecurityError.new('No key was found to decrypt the content master key.') unless private_key
|
93
|
-
content_master_key = private_key.private_decrypt(encrypted_key)
|
94
|
-
|
95
|
-
content_encryption_key = derive_content_key('Encryption', content_master_key, encryption, digest, aes_length)
|
96
|
-
content_integrity_key = derive_content_key('Integrity', content_master_key, encryption, digest, sha_length)
|
97
|
-
|
98
|
-
secured_input = parts.take(4).join('.')
|
99
|
-
computed_integrity_value = OpenSSL::HMAC.digest(digest, content_integrity_key, secured_input)
|
100
|
-
throw ArgumentError.new('Invalid signature.') unless integrity_value == computed_integrity_value
|
101
|
-
|
102
|
-
cipher = OpenSSL::Cipher.new("AES-#{aes_length}-CBC")
|
103
|
-
cipher.decrypt
|
104
|
-
cipher.key = content_encryption_key
|
105
|
-
cipher.iv = iv
|
106
|
-
cipher.update(ciphertext) + cipher.final
|
107
|
-
when 'A128GCM', 'A256GCM'
|
108
|
-
throw NotImplementedError.new("The GCM family of encryption algorithms are not implemented yet.")
|
109
|
-
else
|
110
|
-
throw NotImplementedError.new("The #{encryption} encryption algorithm is not supported.")
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
private
|
115
|
-
|
116
|
-
# Derives content keys using the Concat KDF.
|
117
|
-
def self.derive_content_key(label, content_master_key, encryption, digest, size)
|
118
|
-
round_number = [1].pack('N')
|
119
|
-
output_size = [size].pack('N')
|
120
|
-
enc_bytes = encryption.encode('utf-8').bytes.to_a.pack('C*')
|
121
|
-
epu = epv = [0].pack('N')
|
122
|
-
label_bytes = label.encode('us-ascii').bytes.to_a.pack('C*')
|
123
|
-
hash_input = round_number + content_master_key + output_size + enc_bytes + epu + epv + label_bytes
|
124
|
-
hash = digest.digest(hash_input)
|
125
|
-
hash[0..((size / 8) - 1)]
|
82
|
+
enc = enc_finder.call(header)
|
83
|
+
throw SecurityError.new('No decryptor was found.') unless enc
|
84
|
+
enc.decrypt(encrypted_key, iv, ciphertext, parts.take(4).join('.'), integrity_value)
|
126
85
|
end
|
127
86
|
|
128
87
|
end
|
@@ -149,12 +108,12 @@ if __FILE__ == $0
|
|
149
108
|
puts jws_token
|
150
109
|
|
151
110
|
jwe_key = OpenSSL::PKey::RSA.new(2048)
|
152
|
-
enc = Sandal::Enc::
|
111
|
+
enc = Sandal::Enc::AES128GCM.new(jwe_key.public_key)
|
153
112
|
jwe_token = Sandal.encrypt_token(jws_token, enc, { 'cty' => 'JWT' })
|
154
113
|
|
155
114
|
puts jwe_token
|
156
115
|
|
157
|
-
jws_token_2 = Sandal.decrypt_token(jwe_token) { |header| jwe_key }
|
116
|
+
jws_token_2 = Sandal.decrypt_token(jwe_token) { |header| Sandal::Enc::AES128CBC.new(jwe_key) }
|
158
117
|
roundtrip_claims = Sandal.decode_token(jws_token_2) { |header| Sandal::Sig::RS256.new(jws_key.public_key) }
|
159
118
|
|
160
119
|
puts roundtrip_claims
|
data/lib/sandal/enc.rb
CHANGED
@@ -14,11 +14,12 @@ module Sandal
|
|
14
14
|
end
|
15
15
|
|
16
16
|
# Decrypts a token.
|
17
|
-
def decrypt(
|
17
|
+
def decrypt(encrypted_key, iv, ciphertext, secured_input, integrity_value)
|
18
18
|
throw NotImplementedError.new("#{@name}.decrypt is not implemented.")
|
19
19
|
end
|
20
20
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
require 'sandal/enc/aescbc'
|
24
|
+
require 'sandal/enc/aescbc'
|
25
|
+
require 'sandal/enc/aesgcm'
|
data/lib/sandal/enc/aescbc.rb
CHANGED
@@ -44,6 +44,19 @@ module Sandal
|
|
44
44
|
[secured_input, encoded_integrity_value].join('.')
|
45
45
|
end
|
46
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
|
+
throw ArgumentError.new('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
|
+
|
47
60
|
private
|
48
61
|
|
49
62
|
# Derives content keys using the Concat KDF.
|
@@ -0,0 +1,40 @@
|
|
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
|
+
throw NotImplementedException.new('AES-CGM is not yet implemented.')
|
13
|
+
end
|
14
|
+
|
15
|
+
def encrypt(header, payload)
|
16
|
+
throw NotImplementedException.new('AES-CGM is not yet implemented.')
|
17
|
+
end
|
18
|
+
|
19
|
+
def decrypt(encrypted_key, iv, ciphertext, secured_input, integrity_value)
|
20
|
+
throw NotImplementedException.new('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
|
data/lib/sandal/sig.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
1
3
|
module Sandal
|
2
4
|
# Common signature traits.
|
3
5
|
module Sig
|
@@ -18,6 +20,7 @@ module Sandal
|
|
18
20
|
# The 'none' JWA signature method.
|
19
21
|
class None
|
20
22
|
include Sandal::Sig
|
23
|
+
include Singleton
|
21
24
|
|
22
25
|
# Creates a new instance.
|
23
26
|
def initialize
|
@@ -39,5 +42,6 @@ module Sandal
|
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
45
|
+
require 'sandal/sig/es'
|
42
46
|
require 'sandal/sig/hs'
|
43
47
|
require 'sandal/sig/rs'
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Sandal
|
4
|
+
module Sig
|
5
|
+
|
6
|
+
# Base implementation of the ECDSA-SHA family of signature algorithms.
|
7
|
+
class ES
|
8
|
+
include Sandal::Sig
|
9
|
+
|
10
|
+
# Creates a new instance with the size of the SHA algorithm and an OpenSSL ES PKey.
|
11
|
+
def initialize(sha_size, key)
|
12
|
+
throw ArgumentError.new('A key is required.') unless key
|
13
|
+
@name = "ES#{sha_size}"
|
14
|
+
@digest = OpenSSL::Digest.new("SHA#{sha_size}")
|
15
|
+
@key = key
|
16
|
+
end
|
17
|
+
|
18
|
+
# Signs a payload and returns the signature.
|
19
|
+
def sign(payload)
|
20
|
+
hash = @digest.digest(payload)
|
21
|
+
asn1_sig = @key.dsa_sign_asn1(hash)
|
22
|
+
r, s = self.class.decode_asn1_signature(asn1_sig)
|
23
|
+
self.class.encode_jws_signature(r, s)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Verifies a payload signature and returns whether the signature matches.
|
27
|
+
def verify(signature, payload)
|
28
|
+
hash = @digest.digest(payload)
|
29
|
+
r, s = self.class.decode_jws_signature(signature)
|
30
|
+
asn1_sig = self.class.encode_asn1_signature(r, s)
|
31
|
+
@key.dsa_verify_asn1(hash, asn1_sig)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Decodes an ASN1 signature into a pair of BNs.
|
35
|
+
def self.decode_asn1_signature(signature)
|
36
|
+
asn_seq = OpenSSL::ASN1.decode(signature)
|
37
|
+
return asn_seq.value[0].value, asn_seq.value[1].value
|
38
|
+
end
|
39
|
+
|
40
|
+
# Encodes a pair of BNs into an ASN1 signature.
|
41
|
+
def self.encode_asn1_signature(r, s)
|
42
|
+
items = [OpenSSL::ASN1::Integer.new(r), OpenSSL::ASN1::Integer.new(s)]
|
43
|
+
OpenSSL::ASN1::Sequence.new(items).to_der
|
44
|
+
end
|
45
|
+
|
46
|
+
# Decodes a JWS signature into a pair of BNs.
|
47
|
+
def self.decode_jws_signature(signature)
|
48
|
+
hex_string = Sandal::Util.base64_decode(signature)
|
49
|
+
coord_length = hex_string.length / 2
|
50
|
+
r = OpenSSL::BN.new(hex_string[0..(coord_length - 1)].unpack('H*')[0], 16)
|
51
|
+
s = OpenSSL::BN.new(hex_string[coord_length..-1].unpack('H*')[0], 16)
|
52
|
+
return r, s
|
53
|
+
end
|
54
|
+
|
55
|
+
# Encodes a pair of BNs into a JWS signature.
|
56
|
+
def self.encode_jws_signature(r, s)
|
57
|
+
hex_string = [r.to_s(16) + s.to_s(16)].pack('H*')
|
58
|
+
Sandal::Util.base64_encode(hex_string)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# The ECDSA-SHA256 signing algorithm.
|
64
|
+
class ES256 < Sandal::Sig::ES
|
65
|
+
# Creates a new instance with an OpenSSL ES PKey.
|
66
|
+
def initialize(key)
|
67
|
+
super(256, key)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# The ECDSA-SHA384 signing algorithm.
|
72
|
+
class ES384 < Sandal::Sig::ES
|
73
|
+
# Creates a new instance with an OpenSSL ES PKey.
|
74
|
+
def initialize(key)
|
75
|
+
super(384, key)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# The ECDSA-SHA512 signing algorithm.
|
80
|
+
class ES512 < Sandal::Sig::ES
|
81
|
+
# Creates a new instance with an OpenSSL ES PKey.
|
82
|
+
def initialize(key)
|
83
|
+
super(512, key)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
data/lib/sandal/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sandal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
5
6
|
platform: ruby
|
6
7
|
authors:
|
7
8
|
- Greg Beech
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2013-03-
|
12
|
+
date: 2013-03-25 00:00:00.000000000 Z
|
12
13
|
dependencies: []
|
13
14
|
description: A ruby library for creating and reading JSON Web Tokens (JWT), supporting
|
14
15
|
JSON Web Signatures (JWS) and JSON Web Encryption (JWE).
|
@@ -19,7 +20,9 @@ extensions: []
|
|
19
20
|
extra_rdoc_files: []
|
20
21
|
files:
|
21
22
|
- lib/sandal/enc/aescbc.rb
|
23
|
+
- lib/sandal/enc/aesgcm.rb
|
22
24
|
- lib/sandal/enc.rb
|
25
|
+
- lib/sandal/sig/es.rb
|
23
26
|
- lib/sandal/sig/hs.rb
|
24
27
|
- lib/sandal/sig/rs.rb
|
25
28
|
- lib/sandal/sig.rb
|
@@ -29,25 +32,26 @@ files:
|
|
29
32
|
homepage: http://rubygems.org/gems/sandal
|
30
33
|
licenses:
|
31
34
|
- MIT
|
32
|
-
metadata: {}
|
33
35
|
post_install_message:
|
34
36
|
rdoc_options: []
|
35
37
|
require_paths:
|
36
38
|
- lib
|
37
39
|
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
38
41
|
requirements:
|
39
42
|
- - ! '>='
|
40
43
|
- !ruby/object:Gem::Version
|
41
44
|
version: '0'
|
42
45
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
43
47
|
requirements:
|
44
48
|
- - ! '>='
|
45
49
|
- !ruby/object:Gem::Version
|
46
50
|
version: '0'
|
47
51
|
requirements: []
|
48
52
|
rubyforge_project:
|
49
|
-
rubygems_version:
|
53
|
+
rubygems_version: 1.8.25
|
50
54
|
signing_key:
|
51
|
-
specification_version:
|
55
|
+
specification_version: 3
|
52
56
|
summary: A JSON Web Token (JWT) library.
|
53
57
|
test_files: []
|
checksums.yaml
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
---
|
2
|
-
!binary "U0hBMQ==":
|
3
|
-
metadata.gz: !binary |-
|
4
|
-
NjVlMDRhMzgyZTY4OTkyMDZjZmUwNDY2ZGY3YjY4NTdiZWY0NDFjZA==
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
MjE1N2ZmZmQxMjI1YmEzYzQ4YzJiMGY5MjliOWQ0NDU0NmYzZWM2Mw==
|
7
|
-
!binary "U0hBNTEy":
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
YjU2ZWEwYWU5NDc2MzliNmQwNjExMDdmYTU2ZTc4OGRlZWQ3ZWE4ODAxZmE1
|
10
|
-
MzJkMWJlMzQzZjQ2ZDg4MGY1YTlmZThkZDE5MmUzZTBmMTQ2NjQ1OWFkZTJm
|
11
|
-
MDkzYTY3NGQwNDFmNjlkMGQ4ODliOWFlMjg4MTA3MDAyNWNhMGY=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
NzRiZjIwYjg2OGUwOWZlZmY3MTAxY2U3ZWFmMjI5NjZiM2U1ODFlMjBlNDA0
|
14
|
-
Y2M4NzI2M2IyMGU2ODUyYWE1MTEzYTczMTc5YjM2Yzg1N2IxNWNlYjFlYzk1
|
15
|
-
MmFlMjkxZmU2ODRkNTUxMGZkODNlNWY1ZDQyMDQ1MGUwMmVkNzI=
|