sandal 0.0.0

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.
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: []