jose 0.1.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.
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