sandal 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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=
|