jose 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +20 -0
  3. data/.gitignore +9 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +4 -0
  7. data/CODE_OF_CONDUCT.md +13 -0
  8. data/Gemfile +20 -0
  9. data/LICENSE.txt +373 -0
  10. data/README.md +41 -0
  11. data/Rakefile +10 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +7 -0
  14. data/jose.gemspec +36 -0
  15. data/lib/jose.rb +61 -0
  16. data/lib/jose/jwa.rb +21 -0
  17. data/lib/jose/jwa/aes_kw.rb +105 -0
  18. data/lib/jose/jwa/concat_kdf.rb +50 -0
  19. data/lib/jose/jwa/pkcs1.rb +269 -0
  20. data/lib/jose/jwa/pkcs7.rb +23 -0
  21. data/lib/jose/jwe.rb +290 -0
  22. data/lib/jose/jwe/alg.rb +12 -0
  23. data/lib/jose/jwe/alg_aes_gcm_kw.rb +98 -0
  24. data/lib/jose/jwe/alg_aes_kw.rb +57 -0
  25. data/lib/jose/jwe/alg_dir.rb +40 -0
  26. data/lib/jose/jwe/alg_ecdh_es.rb +123 -0
  27. data/lib/jose/jwe/alg_pbes2.rb +90 -0
  28. data/lib/jose/jwe/alg_rsa.rb +63 -0
  29. data/lib/jose/jwe/enc.rb +8 -0
  30. data/lib/jose/jwe/enc_aes_cbc_hmac.rb +80 -0
  31. data/lib/jose/jwe/enc_aes_gcm.rb +68 -0
  32. data/lib/jose/jwe/zip.rb +7 -0
  33. data/lib/jose/jwe/zip_def.rb +28 -0
  34. data/lib/jose/jwk.rb +347 -0
  35. data/lib/jose/jwk/kty.rb +34 -0
  36. data/lib/jose/jwk/kty_ec.rb +179 -0
  37. data/lib/jose/jwk/kty_oct.rb +104 -0
  38. data/lib/jose/jwk/kty_rsa.rb +185 -0
  39. data/lib/jose/jwk/pem.rb +19 -0
  40. data/lib/jose/jwk/set.rb +2 -0
  41. data/lib/jose/jws.rb +242 -0
  42. data/lib/jose/jws/alg.rb +10 -0
  43. data/lib/jose/jws/alg_ecdsa.rb +41 -0
  44. data/lib/jose/jws/alg_hmac.rb +41 -0
  45. data/lib/jose/jws/alg_rsa_pkcs1_v1_5.rb +41 -0
  46. data/lib/jose/jws/alg_rsa_pss.rb +41 -0
  47. data/lib/jose/jwt.rb +145 -0
  48. data/lib/jose/version.rb +3 -0
  49. metadata +162 -0
@@ -0,0 +1,23 @@
1
+ module JOSE::JWA::PKCS7
2
+
3
+ extend self
4
+
5
+ def pad(binary)
6
+ size = 16 - (binary.bytesize % 16)
7
+ return [binary, *([size] * size)].pack('a*C*')
8
+ end
9
+
10
+ def unpad(binary)
11
+ p = binary.getbyte(-1)
12
+ size = binary.bytesize - p
13
+ binary_s = StringIO.new(binary)
14
+ result = binary_s.read(size)
15
+ p.times do
16
+ if binary_s.getbyte != p
17
+ raise ArgumentError, "unrecognized padding"
18
+ end
19
+ end
20
+ return result
21
+ end
22
+
23
+ end
data/lib/jose/jwe.rb ADDED
@@ -0,0 +1,290 @@
1
+ module JOSE
2
+
3
+ class EncryptedBinary < ::String
4
+ def expand
5
+ return JOSE::JWE.expand(self)
6
+ end
7
+ end
8
+
9
+ class EncryptedMap < JOSE::Map
10
+ def compact
11
+ return JOSE::JWE.compact(self)
12
+ end
13
+ end
14
+
15
+ class JWE < Struct.new(:alg, :enc, :zip, :fields)
16
+
17
+ # Decode API
18
+
19
+ def self.from(object, modules = {})
20
+ case object
21
+ when JOSE::Map, Hash
22
+ return from_map(object, modules)
23
+ when String
24
+ return from_binary(object, modules)
25
+ when JOSE::JWE
26
+ return object
27
+ else
28
+ raise ArgumentError, "'object' must be a Hash, String, or JOSE::JWE"
29
+ end
30
+ end
31
+
32
+ def self.from_binary(object, modules = {})
33
+ case object
34
+ when String
35
+ return from_map(JOSE.decode(object), modules)
36
+ else
37
+ raise ArgumentError, "'object' must be a String"
38
+ end
39
+ end
40
+
41
+ def self.from_file(file, modules = {})
42
+ return from_binary(File.binread(file), modules)
43
+ end
44
+
45
+ def self.from_map(object, modules = {})
46
+ case object
47
+ when JOSE::Map, Hash
48
+ return from_fields(JOSE::JWE.new(nil, nil, nil, JOSE::Map.new(object)), modules)
49
+ else
50
+ raise ArgumentError, "'object' must be a Hash"
51
+ end
52
+ end
53
+
54
+ # Encode API
55
+
56
+ def self.to_binary(jwe)
57
+ return from(jwe).to_binary
58
+ end
59
+
60
+ def to_binary
61
+ return JOSE.encode(to_map)
62
+ end
63
+
64
+ def self.to_file(jwe, file)
65
+ return from(jwe).to_file(file)
66
+ end
67
+
68
+ def to_file(file)
69
+ return File.binwrite(file, to_binary)
70
+ end
71
+
72
+ def self.to_map(jwe)
73
+ return from(jwe).to_map
74
+ end
75
+
76
+ def to_map
77
+ if zip.nil?
78
+ return alg.to_map(enc.to_map(fields))
79
+ else
80
+ return alg.to_map(enc.to_map(zip.to_map(fields)))
81
+ end
82
+ end
83
+
84
+ # API
85
+
86
+ def self.block_decrypt(key, encrypted)
87
+ if encrypted.is_a?(String)
88
+ encrypted = JOSE::JWE.expand(encrypted)
89
+ end
90
+ if encrypted.is_a?(Hash)
91
+ encrypted = JOSE::EncryptedMap.new(encrypted)
92
+ end
93
+ if encrypted.is_a?(JOSE::Map) and encrypted['ciphertext'].is_a?(String) and encrypted['encrypted_key'].is_a?(String) and encrypted['iv'].is_a?(String) and encrypted['protected'].is_a?(String) and encrypted['tag'].is_a?(String)
94
+ jwe = from_binary(JOSE.urlsafe_decode64(encrypted['protected']))
95
+ encrypted_key = JOSE.urlsafe_decode64(encrypted['encrypted_key'])
96
+ iv = JOSE.urlsafe_decode64(encrypted['iv'])
97
+ cipher_text = JOSE.urlsafe_decode64(encrypted['ciphertext'])
98
+ cipher_tag = JOSE.urlsafe_decode64(encrypted['tag'])
99
+ if encrypted['aad'].is_a?(String)
100
+ concat_aad = [encrypted['protected'], '.', encrypted['aad']].join
101
+ return jwe.block_decrypt(key, concat_aad, cipher_text, cipher_tag, encrypted_key, iv), jwe
102
+ else
103
+ return jwe.block_decrypt(key, encrypted['protected'], cipher_text, cipher_tag, encrypted_key, iv), jwe
104
+ end
105
+ else
106
+ raise ArgumentError, "'encrypted' is not a valid encrypted String, Hash, or JOSE::Map"
107
+ end
108
+ end
109
+
110
+ def block_decrypt(key, aad, cipher_text, cipher_tag, encrypted_key, iv)
111
+ cek = key_decrypt(key, encrypted_key)
112
+ return uncompress(enc.block_decrypt([aad, cipher_text, cipher_tag], cek, iv))
113
+ end
114
+
115
+ def self.block_encrypt(key, block, jwe, cek = nil, iv = nil)
116
+ return from(jwe).block_encrypt(key, block, cek, iv)
117
+ end
118
+
119
+ def block_encrypt(key, block, cek = nil, iv = nil)
120
+ cek ||= next_cek(key)
121
+ iv ||= next_iv
122
+ aad, plain_text = block
123
+ if plain_text.nil?
124
+ plain_text = aad
125
+ aad = nil
126
+ end
127
+ encrypted_key, jwe = key_encrypt(key, cek)
128
+ protected_binary = JOSE.urlsafe_encode64(jwe.to_binary)
129
+ if aad.nil?
130
+ cipher_text, cipher_tag = enc.block_encrypt([protected_binary, jwe.compress(plain_text)], cek, iv)
131
+ return JOSE::EncryptedMap[
132
+ 'ciphertext' => JOSE.urlsafe_encode64(cipher_text),
133
+ 'encrypted_key' => JOSE.urlsafe_encode64(encrypted_key),
134
+ 'iv' => JOSE.urlsafe_encode64(iv),
135
+ 'protected' => protected_binary,
136
+ 'tag' => JOSE.urlsafe_encode64(cipher_tag)
137
+ ]
138
+ else
139
+ aad_b64 = JOSE.urlsafe_encode64(aad)
140
+ concat_aad = [protected_binary, '.', aad_b64].join
141
+ cipher_text, cipher_tag = enc.block_encrypt([aad_b64, jwe.compress(plain_text)], cek, iv)
142
+ return JOSE::EncryptedMap[
143
+ 'aad' => aad_b64,
144
+ 'ciphertext' => JOSE.urlsafe_encode64(cipher_text),
145
+ 'encrypted_key' => JOSE.urlsafe_encode64(encrypted_key),
146
+ 'iv' => JOSE.urlsafe_encode64(iv),
147
+ 'protected' => protected_binary,
148
+ 'tag' => JOSE.urlsafe_encode64(cipher_tag)
149
+ ]
150
+ end
151
+ end
152
+
153
+ def self.compact(map)
154
+ if map.is_a?(Hash) or map.is_a?(JOSE::Map)
155
+ if map.has_key?('aad')
156
+ raise ArgumentError, "'map' with 'aad' cannot be compacted"
157
+ end
158
+ return JOSE::EncryptedBinary.new([
159
+ map['protected'] || '',
160
+ '.',
161
+ map['encrypted_key'] || '',
162
+ '.',
163
+ map['iv'] || '',
164
+ '.',
165
+ map['ciphertext'] || '',
166
+ '.',
167
+ map['tag'] || ''
168
+ ].join)
169
+ else
170
+ raise ArgumentError, "'map' must be a Hash or a JOSE::Map"
171
+ end
172
+ end
173
+
174
+ def compress(plain_text)
175
+ if zip.nil?
176
+ return plain_text
177
+ else
178
+ return zip.compress(plain_text)
179
+ end
180
+ end
181
+
182
+ def self.expand(binary)
183
+ if binary.is_a?(String)
184
+ parts = binary.split('.')
185
+ if parts.length == 5
186
+ protected_binary, encrypted_key, initialization_vector, cipher_text, authentication_tag = parts
187
+ return JOSE::EncryptedMap[
188
+ 'ciphertext' => cipher_text,
189
+ 'encrypted_key' => encrypted_key,
190
+ 'iv' => initialization_vector,
191
+ 'protected' => protected_binary,
192
+ 'tag' => authentication_tag
193
+ ]
194
+ else
195
+ raise ArgumentError, "'binary' is not a valid encrypted String"
196
+ end
197
+ else
198
+ raise ArgumentError, "'binary' must be a String"
199
+ end
200
+ end
201
+
202
+ def key_decrypt(key, encrypted_key)
203
+ return alg.key_decrypt(key, enc, encrypted_key)
204
+ end
205
+
206
+ def key_encrypt(key, decrypted_key)
207
+ encrypted_key, new_alg = alg.key_encrypt(key, enc, decrypted_key)
208
+ new_jwe = JOSE::JWE.from_map(to_map)
209
+ new_jwe.alg = new_alg
210
+ return encrypted_key, new_jwe
211
+ end
212
+
213
+ def next_cek(key)
214
+ return alg.next_cek(key, enc)
215
+ end
216
+
217
+ def next_iv
218
+ return enc.next_iv
219
+ end
220
+
221
+ def self.peek_protected(encrypted)
222
+ if encrypted.is_a?(String)
223
+ encrypted = expand(encrypted)
224
+ end
225
+ return JOSE::Map.new(JOSE.decode(JOSE.urlsafe_decode64(encrypted['protected'])))
226
+ end
227
+
228
+ def uncompress(cipher_text)
229
+ if zip.nil?
230
+ return cipher_text
231
+ else
232
+ return zip.uncompress(cipher_text)
233
+ end
234
+ end
235
+
236
+ private
237
+
238
+ def self.from_fields(jwe, modules)
239
+ if jwe.fields.has_key?('alg')
240
+ alg = modules[:alg] || case jwe.fields['alg']
241
+ when 'A128KW', 'A192KW', 'A256KW'
242
+ JOSE::JWE::ALG_AES_KW
243
+ when 'A128GCMKW', 'A192GCMKW', 'A256GCMKW'
244
+ JOSE::JWE::ALG_AES_GCM_KW
245
+ when 'dir'
246
+ JOSE::JWE::ALG_dir
247
+ when 'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW'
248
+ JOSE::JWE::ALG_ECDH_ES
249
+ when 'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW'
250
+ JOSE::JWE::ALG_PBES2
251
+ when 'RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256'
252
+ JOSE::JWE::ALG_RSA
253
+ else
254
+ raise ArgumentError, "unknown 'alg': #{jwe.fields['alg'].inspect}"
255
+ end
256
+ jwe.alg, jwe.fields = alg.from_map(jwe.fields)
257
+ return from_fields(jwe, modules)
258
+ elsif jwe.fields.has_key?('enc')
259
+ enc = modules[:enc] || case jwe.fields['enc']
260
+ when 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512'
261
+ JOSE::JWE::ENC_AES_CBC_HMAC
262
+ when 'A128GCM', 'A192GCM', 'A256GCM'
263
+ JOSE::JWE::ENC_AES_GCM
264
+ else
265
+ raise ArgumentError, "unknown 'enc': #{jwe.fields['enc'].inspect}"
266
+ end
267
+ jwe.enc, jwe.fields = enc.from_map(jwe.fields)
268
+ return from_fields(jwe, modules)
269
+ elsif jwe.fields.has_key?('zip')
270
+ zip = modules[:zip] || case jwe.fields['zip']
271
+ when 'DEF'
272
+ JOSE::JWE::ZIP_DEF
273
+ else
274
+ raise ArgumentError, "unknown 'zip': #{jwe.fields['zip'].inspect}"
275
+ end
276
+ jwe.zip, jwe.fields = zip.from_map(jwe.fields)
277
+ return from_fields(jwe, modules)
278
+ elsif jwe.alg.nil? and jwe.enc.nil?
279
+ raise ArgumentError, "missing required keys: 'alg' and 'enc'"
280
+ else
281
+ return jwe
282
+ end
283
+ end
284
+
285
+ end
286
+ end
287
+
288
+ require 'jose/jwe/alg'
289
+ require 'jose/jwe/enc'
290
+ require 'jose/jwe/zip'
@@ -0,0 +1,12 @@
1
+ module JOSE::JWE::ALG
2
+
3
+ extend self
4
+
5
+ end
6
+
7
+ require 'jose/jwe/alg_aes_gcm_kw'
8
+ require 'jose/jwe/alg_aes_kw'
9
+ require 'jose/jwe/alg_dir'
10
+ require 'jose/jwe/alg_ecdh_es'
11
+ require 'jose/jwe/alg_pbes2'
12
+ require 'jose/jwe/alg_rsa'
@@ -0,0 +1,98 @@
1
+ class JOSE::JWE::ALG_AES_GCM_KW < Struct.new(:cipher_name, :bits, :iv, :tag)
2
+
3
+ # JOSE::JWE callbacks
4
+
5
+ def self.from_map(fields)
6
+ bits = nil
7
+ cipher_name = nil
8
+ case fields['alg']
9
+ when 'A128GCMKW'
10
+ bits = 128
11
+ cipher_name = 'aes-128-gcm'
12
+ when 'A192GCMKW'
13
+ bits = 192
14
+ cipher_name = 'aes-192-gcm'
15
+ when 'A256GCMKW'
16
+ bits = 256
17
+ cipher_name = 'aes-256-gcm'
18
+ else
19
+ raise ArgumentError, "invalid 'alg' for JWE: #{fields['alg'].inspect}"
20
+ end
21
+ iv = nil
22
+ if fields.has_key?('iv')
23
+ iv = JOSE.urlsafe_decode64(fields['iv'])
24
+ end
25
+ tag = nil
26
+ if fields.has_key?('tag')
27
+ tag = JOSE.urlsafe_decode64(fields['tag'])
28
+ end
29
+ return new(cipher_name, bits, iv, tag), fields.except('alg', 'iv', 'tag')
30
+ end
31
+
32
+ def to_map(fields)
33
+ alg = case bits
34
+ when 128
35
+ 'A128GCMKW'
36
+ when 192
37
+ 'A192GCMKW'
38
+ when 256
39
+ 'A256GCMKW'
40
+ else
41
+ raise ArgumentError, "unhandled JOSE::JWE::ALG_AES_KW bits: #{bits.inspect}"
42
+ end
43
+ fields = fields.put('alg', alg)
44
+ if iv
45
+ fields = fields.put('iv', JOSE.urlsafe_encode64(iv))
46
+ end
47
+ if tag
48
+ fields = fields.put('tag', JOSE.urlsafe_encode64(tag))
49
+ end
50
+ return fields
51
+ end
52
+
53
+ # JOSE::JWE::ALG callbacks
54
+
55
+ def key_decrypt(key, enc, encrypted_key)
56
+ if iv.nil? or tag.nil?
57
+ raise ArgumentError, "missing required fields for decryption: 'iv' and 'tag'"
58
+ end
59
+ if key.is_a?(JOSE::JWK)
60
+ key = key.kty.derive_key
61
+ end
62
+ derived_key = key
63
+ aad = ''
64
+ cipher_text = encrypted_key
65
+ cipher_tag = tag
66
+ cipher = OpenSSL::Cipher.new(cipher_name)
67
+ cipher.decrypt
68
+ cipher.key = derived_key
69
+ cipher.iv = iv
70
+ cipher.auth_data = aad
71
+ cipher.auth_tag = cipher_tag
72
+ plain_text = cipher.update(cipher_text) + cipher.final
73
+ return plain_text
74
+ end
75
+
76
+ def key_encrypt(key, enc, decrypted_key)
77
+ if key.is_a?(JOSE::JWK)
78
+ key = key.kty.derive_key
79
+ end
80
+ new_alg = JOSE::JWE::ALG_AES_GCM_KW.new(cipher_name, bits, iv || SecureRandom.random_bytes(12))
81
+ derived_key = key
82
+ aad = ''
83
+ plain_text = decrypted_key
84
+ cipher = OpenSSL::Cipher.new(new_alg.cipher_name)
85
+ cipher.encrypt
86
+ cipher.key = derived_key
87
+ cipher.iv = new_alg.iv
88
+ cipher.auth_data = aad
89
+ cipher_text = cipher.update(plain_text) + cipher.final
90
+ new_alg.tag = cipher.auth_tag
91
+ return cipher_text, new_alg
92
+ end
93
+
94
+ def next_cek(key, enc)
95
+ return enc.next_cek
96
+ end
97
+
98
+ end
@@ -0,0 +1,57 @@
1
+ class JOSE::JWE::ALG_AES_KW < Struct.new(:bits)
2
+
3
+ # JOSE::JWE callbacks
4
+
5
+ def self.from_map(fields)
6
+ bits = case fields['alg']
7
+ when 'A128KW'
8
+ 128
9
+ when 'A192KW'
10
+ 192
11
+ when 'A256KW'
12
+ 256
13
+ else
14
+ raise ArgumentError, "invalid 'alg' for JWE: #{fields['alg'].inspect}"
15
+ end
16
+ return new(bits), fields.except('alg')
17
+ end
18
+
19
+ def to_map(fields)
20
+ alg = case bits
21
+ when 128
22
+ 'A128KW'
23
+ when 192
24
+ 'A192KW'
25
+ when 256
26
+ 'A256KW'
27
+ else
28
+ raise ArgumentError, "unhandled JOSE::JWE::ALG_AES_KW bits: #{bits.inspect}"
29
+ end
30
+ return fields.put('alg', alg)
31
+ end
32
+
33
+ # JOSE::JWE::ALG callbacks
34
+
35
+ def key_decrypt(key, enc, encrypted_key)
36
+ if key.is_a?(JOSE::JWK)
37
+ key = key.kty.derive_key
38
+ end
39
+ derived_key = key
40
+ decrypted_key = JOSE::JWA::AES_KW.unwrap(encrypted_key, derived_key)
41
+ return decrypted_key
42
+ end
43
+
44
+ def key_encrypt(key, enc, decrypted_key)
45
+ if key.is_a?(JOSE::JWK)
46
+ key = key.kty.derive_key
47
+ end
48
+ derived_key = key
49
+ encrypted_key = JOSE::JWA::AES_KW.wrap(decrypted_key, derived_key)
50
+ return encrypted_key, self
51
+ end
52
+
53
+ def next_cek(key, enc)
54
+ return enc.next_cek
55
+ end
56
+
57
+ end