sandal 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
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=
@@ -0,0 +1,78 @@
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
+ throw ArgumentError.new('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(JSON.generate(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
+ private
48
+
49
+ # Derives content keys using the Concat KDF.
50
+ def derive_content_key(label, content_master_key, size)
51
+ round_number = [1].pack('N')
52
+ output_size = [size].pack('N')
53
+ enc_bytes = @name.encode('utf-8').bytes.to_a.pack('C*')
54
+ epu = epv = [0].pack('N')
55
+ label_bytes = label.encode('us-ascii').bytes.to_a.pack('C*')
56
+ hash_input = round_number + content_master_key + output_size + enc_bytes + epu + epv + label_bytes
57
+ hash = @digest.digest(hash_input)
58
+ hash[0..((size / 8) - 1)]
59
+ end
60
+
61
+ end
62
+
63
+ # The AES-128-CBC encryption algorithm.
64
+ class AES128CBC < Sandal::Enc::AESCBC
65
+ def initialize(key)
66
+ super(128, key)
67
+ end
68
+ end
69
+
70
+ # The AES-256-CBC encryption algorithm.
71
+ class AES256CBC < Sandal::Enc::AESCBC
72
+ def initialize(key)
73
+ super(256, key)
74
+ end
75
+ end
76
+
77
+ end
78
+ end
data/lib/sandal/enc.rb ADDED
@@ -0,0 +1,24 @@
1
+ module Sandal
2
+ # Common encryption traits.
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
+ throw NotImplementedError.new("#{@name}.encrypt is not implemented.")
14
+ end
15
+
16
+ # Decrypts a token.
17
+ def decrypt(data)
18
+ throw NotImplementedError.new("#{@name}.decrypt is not implemented.")
19
+ end
20
+
21
+ end
22
+ end
23
+
24
+ require 'sandal/enc/aescbc'
@@ -0,0 +1,55 @@
1
+ require 'openssl'
2
+
3
+ module Sandal
4
+ module Sig
5
+
6
+ # Base implementation of the HMAC-SHA family of signature algorithms.
7
+ class HS
8
+ include Sandal::Sig
9
+
10
+ # Creates a new instance with the size of the SHA algorithm and a string key.
11
+ def initialize(sha_size, key)
12
+ throw ArgumentError.new('A key is required.') unless key
13
+ @name = "HS#{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
+ OpenSSL::HMAC.digest(@digest, @key, payload)
21
+ end
22
+
23
+ # Verifies a payload signature and returns whether the signature matches.
24
+ def verify(signature, payload)
25
+ Sandal::Util.secure_equals(sign(payload), signature)
26
+ end
27
+
28
+ end
29
+
30
+ # The HMAC-SHA256 signing algorithm.
31
+ class HS256 < Sandal::Sig::HS
32
+ # Creates a new instance with a string key.
33
+ def initialize(key)
34
+ super(256, key)
35
+ end
36
+ end
37
+
38
+ # The HMAC-SHA384 signing algorithm.
39
+ class HS384 < Sandal::Sig::HS
40
+ # Creates a new instance with a string key.
41
+ def initialize(key)
42
+ super(384, key)
43
+ end
44
+ end
45
+
46
+ # The HMAC-SHA512 signing algorithm.
47
+ class HS512 < Sandal::Sig::HS
48
+ # Creates a new instance with a string key.
49
+ def initialize(key)
50
+ super(512, key)
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,59 @@
1
+ require 'openssl'
2
+
3
+ module Sandal
4
+ module Sig
5
+
6
+ # Base implementation of the RSA-SHA family of signature algorithms.
7
+ class RS
8
+ include Sandal::Sig
9
+
10
+ # Creates a new instance with the size of the SHA algorithm and an OpenSSL RSA PKey. To sign
11
+ # a value this must contain a private key; to verify a signature a public key is sufficient.
12
+ # Note that the size of the RSA key must be at least 2048 bits to be compliant with the
13
+ # JWA specification.
14
+ def initialize(sha_size, key)
15
+ throw ArgumentError.new('A key is required.') unless key
16
+ @name = "RS#{sha_size}"
17
+ @digest = OpenSSL::Digest.new("SHA#{sha_size}")
18
+ @key = key
19
+ end
20
+
21
+ # Signs a payload and returns the signature.
22
+ def sign(payload)
23
+ throw ArgumentError.new('A private key is required to sign the payload.') unless @key.private?
24
+ @key.sign(@digest, payload)
25
+ end
26
+
27
+ # Verifies a payload signature and returns whether the signature matches.
28
+ def verify(signature, payload)
29
+ @key.verify(@digest, signature, payload)
30
+ end
31
+
32
+ end
33
+
34
+ # The RSA-SHA256 signing algorithm.
35
+ class RS256 < Sandal::Sig::RS
36
+ # Creates a new instance with an OpenSSL RSA PKey.
37
+ def initialize(key)
38
+ super(256, key)
39
+ end
40
+ end
41
+
42
+ # The RSA-SHA384 signing algorithm.
43
+ class RS384 < Sandal::Sig::RS
44
+ # Creates a new instance with an OpenSSL RSA PKey.
45
+ def initialize(key)
46
+ super(384, key)
47
+ end
48
+ end
49
+
50
+ # The RSA-SHA512 signing algorithm.
51
+ class RS512 < Sandal::Sig::RS
52
+ # Creates a new instance with an OpenSSL RSA PKey.
53
+ def initialize(key)
54
+ super(512, key)
55
+ end
56
+ end
57
+
58
+ end
59
+ end
data/lib/sandal/sig.rb ADDED
@@ -0,0 +1,43 @@
1
+ module Sandal
2
+ # Common signature traits.
3
+ module Sig
4
+
5
+ # The JWA name of the algorithm.
6
+ attr_reader :name
7
+
8
+ # Signs a payload and returns the signature.
9
+ def sign(payload)
10
+ throw NotImplementedError.new("#{@name}.sign is not implemented.")
11
+ end
12
+
13
+ # Verifies a payload signature and returns whether the signature matches.
14
+ def verify(signature, payload)
15
+ throw NotImplementedError.new("#{@name}.verify is not implemented.")
16
+ end
17
+
18
+ # The 'none' JWA signature method.
19
+ class None
20
+ include Sandal::Sig
21
+
22
+ # Creates a new instance.
23
+ def initialize
24
+ @name = 'none'
25
+ end
26
+
27
+ # Returns an empty signature.
28
+ def sign(payload)
29
+ ''
30
+ end
31
+
32
+ # Verifies that the signature is empty.
33
+ def verify(signature, payload)
34
+ signature.nil? || signature.length == 0
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
41
+
42
+ require 'sandal/sig/hs'
43
+ require 'sandal/sig/rs'
@@ -0,0 +1,36 @@
1
+ require 'base64'
2
+
3
+ module Sandal
4
+ # Implements some JWT utility functions. Shouldn't be needed by most people but may
5
+ # be useful if you're developing an extension to the library.
6
+ module Util
7
+
8
+ # A string equality function which doesn't short-circuit the equality check to help
9
+ # protect against timing attacks.
10
+ #--
11
+ # See http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/
12
+ def self.secure_equals(a, b)
13
+ if a.nil? && b.nil?
14
+ true
15
+ elsif a.nil? || b.nil? || a.bytesize != b.bytesize
16
+ false
17
+ else
18
+ result = a.bytes.zip(b.bytes).reduce(0) { |memo, (b1, b2)| memo |= (b1 ^ b2) }
19
+ result == 0
20
+ end
21
+ end
22
+
23
+ # Base64 encodes a string, in compliance with the JWT specification.
24
+ def self.base64_encode(s)
25
+ Base64.urlsafe_encode64(s).gsub(%r{=+$}, '')
26
+ end
27
+
28
+ # Base64 decodes a string, in compliance with the JWT specification.
29
+ def self.base64_decode(s)
30
+ padding_length = (4 - (s.length % 4)) % 4
31
+ padding = '=' * padding_length
32
+ Base64.urlsafe_decode64(s + padding)
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,4 @@
1
+ module Sandal
2
+ # The semantic version of the library.
3
+ VERSION = '0.0.0'
4
+ end
data/lib/sandal.rb ADDED
@@ -0,0 +1,162 @@
1
+ $:.unshift('.')
2
+
3
+ require 'base64'
4
+ require 'json'
5
+ require 'openssl'
6
+
7
+ require 'sandal/version'
8
+ require 'sandal/sig'
9
+ require 'sandal/enc'
10
+
11
+ # A library for creating and reading JSON Web Tokens (JWT).
12
+ module Sandal
13
+
14
+ # Creates a signed token.
15
+ def self.encode_token(payload, sig, header_fields = nil)
16
+ if header_fields && header_fields['enc']
17
+ throw ArgumentError.new('The header cannot contain an "enc" parameter.')
18
+ end
19
+ sig ||= Sandal::Sig::None.new
20
+
21
+ header = {}
22
+ header['alg'] = sig.name if sig.name != 'none'
23
+ header = header_fields.merge(header) if header_fields
24
+
25
+ encoded_header = Sandal::Util.base64_encode(JSON.generate(header))
26
+ encoded_payload = Sandal::Util.base64_encode(payload)
27
+ secured_input = [encoded_header, encoded_payload].join('.')
28
+
29
+ signature = sig.sign(secured_input)
30
+ encoded_signature = Sandal::Util.base64_encode(signature)
31
+ [secured_input, encoded_signature].join('.')
32
+ end
33
+
34
+ # Creates an encrypted token.
35
+ def self.encrypt_token(payload, enc, header_fields = nil)
36
+ header = {}
37
+ header['enc'] = enc.name
38
+ header['alg'] = enc.alg_name
39
+ header = header_fields.merge(header) if header_fields
40
+
41
+ enc.encrypt(header, payload)
42
+ end
43
+
44
+ # Decodes a token, verifying the signature if present.
45
+ def self.decode_token(token, &sig_finder)
46
+ parts = token.split('.')
47
+ throw ArgumentError.new('Invalid token format.') unless [2, 3].include?(parts.length)
48
+ begin
49
+ header = JSON.parse(Sandal::Util.base64_decode(parts[0]))
50
+ payload = Sandal::Util.base64_decode(parts[1])
51
+ signature = if parts.length > 2 then Sandal::Util.base64_decode(parts[2]) else '' end
52
+ rescue
53
+ throw ArgumentError.new('Invalid token encoding.')
54
+ end
55
+
56
+ algorithm = header['alg']
57
+ if algorithm && algorithm != 'none'
58
+ throw SecurityError.new('The signature is missing.') unless signature.length > 0
59
+ sig = sig_finder.call(header)
60
+ throw SecurityError.new('No signature verifier was found.') unless sig
61
+ secured_input = parts.take(2).join('.')
62
+ throw ArgumentError.new('Invalid signature.') unless sig.verify(signature, secured_input)
63
+ end
64
+
65
+ payload
66
+ end
67
+
68
+ # Decrypts a token.
69
+ def self.decrypt_token(encrypted_token, &key_finder)
70
+ parts = encrypted_token.split('.')
71
+ throw ArgumentError.new('Invalid token format.') unless parts.length == 5
72
+ begin
73
+ header = JSON.parse(Sandal::Util.base64_decode(parts[0]))
74
+ encrypted_key = Sandal::Util.base64_decode(parts[1])
75
+ iv = Sandal::Util.base64_decode(parts[2])
76
+ ciphertext = Sandal::Util.base64_decode(parts[3])
77
+ integrity_value = Sandal::Util.base64_decode(parts[4])
78
+ rescue
79
+ throw ArgumentError.new('Invalid token encoding.')
80
+ end
81
+
82
+ algorithm = header['alg']
83
+ encryption = header['enc']
84
+ case encryption
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)]
126
+ end
127
+
128
+ end
129
+
130
+ if __FILE__ == $0
131
+
132
+ # create payload
133
+ issued_at = Time.now
134
+ claims = JSON.generate({
135
+ iss: 'example.org',
136
+ aud: 'example.com',
137
+ sub: 'user@example.org',
138
+ iat: issued_at.to_i,
139
+ exp: (issued_at + 3600).to_i
140
+ })
141
+
142
+ puts claims.to_s
143
+
144
+ # sign and encrypt
145
+ jws_key = OpenSSL::PKey::RSA.new(2048)
146
+ sig = Sandal::Sig::RS256.new(jws_key)
147
+ jws_token = Sandal.encode_token(claims.to_s, sig)
148
+
149
+ puts jws_token
150
+
151
+ jwe_key = OpenSSL::PKey::RSA.new(2048)
152
+ enc = Sandal::Enc::AES128CBC.new(jwe_key.public_key)
153
+ jwe_token = Sandal.encrypt_token(jws_token, enc, { 'cty' => 'JWT' })
154
+
155
+ puts jwe_token
156
+
157
+ jws_token_2 = Sandal.decrypt_token(jwe_token) { |header| jwe_key }
158
+ roundtrip_claims = Sandal.decode_token(jws_token_2) { |header| Sandal::Sig::RS256.new(jws_key.public_key) }
159
+
160
+ puts roundtrip_claims
161
+
162
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sandal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Greg Beech
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-03-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A ruby library for creating and reading JSON Web Tokens (JWT), supporting
14
+ JSON Web Signatures (JWS) and JSON Web Encryption (JWE).
15
+ email:
16
+ - greg@gregbeech.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/sandal/enc/aescbc.rb
22
+ - lib/sandal/enc.rb
23
+ - lib/sandal/sig/hs.rb
24
+ - lib/sandal/sig/rs.rb
25
+ - lib/sandal/sig.rb
26
+ - lib/sandal/util.rb
27
+ - lib/sandal/version.rb
28
+ - lib/sandal.rb
29
+ homepage: http://rubygems.org/gems/sandal
30
+ licenses:
31
+ - MIT
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 2.0.3
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: A JSON Web Token (JWT) library.
53
+ test_files: []