jwe 0.3.0 → 0.3.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.
- checksums.yaml +5 -5
- data/.rubocop.yml +6 -2
- data/.travis.yml +12 -8
- data/Gemfile +0 -1
- data/README.md +16 -3
- data/Rakefile +1 -0
- data/jwe.gemspec +2 -1
- data/lib/jwe.rb +65 -30
- data/lib/jwe/alg.rb +12 -4
- data/lib/jwe/alg/a128_kw.rb +1 -0
- data/lib/jwe/alg/a192_kw.rb +1 -0
- data/lib/jwe/alg/a256_kw.rb +1 -0
- data/lib/jwe/alg/aes_kw.rb +36 -25
- data/lib/jwe/alg/dir.rb +1 -0
- data/lib/jwe/alg/rsa15.rb +1 -0
- data/lib/jwe/alg/rsa_oaep.rb +1 -0
- data/lib/jwe/base64.rb +1 -0
- data/lib/jwe/enc.rb +6 -5
- data/lib/jwe/enc/a128cbc_hs256.rb +1 -0
- data/lib/jwe/enc/a128gcm.rb +1 -0
- data/lib/jwe/enc/a192cbc_hs384.rb +1 -0
- data/lib/jwe/enc/a192gcm.rb +1 -0
- data/lib/jwe/enc/a256cbc_hs512.rb +1 -0
- data/lib/jwe/enc/a256gcm.rb +1 -0
- data/lib/jwe/enc/aes_cbc_hs.rb +28 -22
- data/lib/jwe/enc/aes_gcm.rb +18 -17
- data/lib/jwe/enc/cipher.rb +14 -0
- data/lib/jwe/serialization/compact.rb +3 -1
- data/lib/jwe/version.rb +1 -1
- data/lib/jwe/zip.rb +2 -4
- data/lib/jwe/zip/def.rb +2 -2
- data/spec/jwe/enc_spec.rb +2 -2
- data/spec/jwe/serialization_spec.rb +2 -2
- data/spec/jwe/zip_spec.rb +14 -4
- data/spec/jwe_spec.rb +12 -0
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a3be72771de62015739c8c0f4db8b6c39c082ed7a3870fb5dce44870418eb2f1
|
4
|
+
data.tar.gz: 65f2fc6f7c1ee46e6524b1e631f0858898348e3a67a2328f522ca449c9a9c0ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a12f84808b5911b81f4afbf2e3ad615abae6543ead869cf1fac1f27b4270158367312b0e5a05b09b3ebf38780344896fa20cb832c7257386cccf8b6d37bdce86
|
7
|
+
data.tar.gz: 4e76467052e21da130ed4b5cd2501d861f2011fc4b91600eb86715f32eee0182a2e65a2ea17f5ef35f38516821394ed925768cc66f6f620dcfa53deeb20ef8c1
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
language: ruby
|
2
|
+
dist: trusty
|
2
3
|
rvm:
|
3
|
-
- 2.0.0
|
4
|
-
- 2.1.
|
5
|
-
- 2.2.
|
6
|
-
- 2.3.
|
7
|
-
- 2.4.
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
- 2.0.0-p648
|
5
|
+
- 2.1.10
|
6
|
+
- 2.2.7
|
7
|
+
- 2.3.4
|
8
|
+
- 2.4.1
|
9
|
+
|
10
|
+
after_script:
|
11
|
+
- bundle exec codeclimate-test-reporter
|
12
|
+
|
13
|
+
env:
|
14
|
+
CODECLIMATE_REPO_TOKEN: d9854e6b60cf9cbd78bb8036da0a5c63d6178a14a19a8043752dc5fecec99831
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# JWE
|
2
2
|
|
3
3
|
[](https://travis-ci.org/jwt/ruby-jwe)
|
4
|
-
[](https://codeclimate.com/github/jwt/ruby-jwe)
|
5
|
+
[](https://codeclimate.com/github/aomega08/jwe/coverage)
|
6
6
|
|
7
7
|
A ruby implementation of the [RFC 7516 JSON Web Encryption (JWE)](https://tools.ietf.org/html/rfc7516) standard.
|
8
8
|
|
@@ -73,6 +73,20 @@ plaintext = JWE.decrypt(encrypted, key)
|
|
73
73
|
puts plaintext #"The quick brown fox jumps over the lazy dog."
|
74
74
|
```
|
75
75
|
|
76
|
+
This example sets an extra **plaintext** custom header.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
require 'jwe'
|
80
|
+
|
81
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
82
|
+
payload = "The quick brown fox jumps over the lazy dog."
|
83
|
+
|
84
|
+
# In this case we add a copyright line to the headers (it can be anything you like
|
85
|
+
# just remember it is plaintext).
|
86
|
+
encrypted = JWE.encrypt(payload, key, copyright: 'This is my stuff! All rights reserved')
|
87
|
+
puts encrypted
|
88
|
+
```
|
89
|
+
|
76
90
|
## Available Algorithms
|
77
91
|
|
78
92
|
The RFC 7518 JSON Web Algorithms (JWA) spec defines the algorithms for [encryption](https://tools.ietf.org/html/rfc7518#section-5.1)
|
@@ -131,4 +145,3 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
131
145
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
132
146
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
133
147
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
134
|
-
|
data/Rakefile
CHANGED
data/jwe.gemspec
CHANGED
@@ -13,11 +13,12 @@ Gem::Specification.new do |s|
|
|
13
13
|
s.license = 'MIT'
|
14
14
|
|
15
15
|
s.files = `git ls-files`.split("\n")
|
16
|
-
s.require_paths = %w
|
16
|
+
s.require_paths = %w[lib]
|
17
17
|
|
18
18
|
s.required_ruby_version = '>= 2.0.0'
|
19
19
|
|
20
20
|
s.add_development_dependency 'rspec'
|
21
21
|
s.add_development_dependency 'rake'
|
22
22
|
s.add_development_dependency 'simplecov'
|
23
|
+
s.add_development_dependency 'codeclimate-test-reporter'
|
23
24
|
end
|
data/lib/jwe.rb
CHANGED
@@ -9,6 +9,7 @@ require 'jwe/alg'
|
|
9
9
|
require 'jwe/enc'
|
10
10
|
require 'jwe/zip'
|
11
11
|
|
12
|
+
# A ruby implementation of the RFC 7516 JSON Web Encryption (JWE) standard.
|
12
13
|
module JWE
|
13
14
|
class DecodeError < RuntimeError; end
|
14
15
|
class NotImplementedError < RuntimeError; end
|
@@ -19,46 +20,80 @@ module JWE
|
|
19
20
|
VALID_ENC = ['A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM'].freeze
|
20
21
|
VALID_ZIP = ['DEF'].freeze
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
raise ArgumentError.new('The key must not be nil or blank') if key.nil? || (key.is_a?(String) && key.strip == '')
|
23
|
+
class << self
|
24
|
+
def encrypt(payload, key, alg: 'RSA-OAEP', enc: 'A128GCM', **more_headers)
|
25
|
+
header = generate_header(alg, enc, more_headers)
|
26
|
+
check_params(header, key)
|
27
27
|
|
28
|
-
|
29
|
-
header[:zip] = zip if zip && zip != ''
|
28
|
+
payload = apply_zip(header, payload, :compress)
|
30
29
|
|
31
|
-
|
32
|
-
|
30
|
+
cipher = Enc.for(enc)
|
31
|
+
cipher.cek = key if alg == 'dir'
|
33
32
|
|
34
|
-
|
33
|
+
json_hdr = header.to_json
|
34
|
+
ciphertext = cipher.encrypt(payload, Base64.jwe_encode(json_hdr))
|
35
35
|
|
36
|
-
|
37
|
-
|
36
|
+
generate_serialization(json_hdr, Alg.encrypt_cek(alg, key, cipher.cek), ciphertext, cipher)
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
39
|
+
def decrypt(payload, key)
|
40
|
+
header, enc_key, iv, ciphertext, tag = Serialization::Compact.decode(payload)
|
41
|
+
header = JSON.parse(header)
|
42
|
+
check_params(header, key)
|
43
|
+
|
44
|
+
cek = Alg.decrypt_cek(header['alg'], key, enc_key)
|
45
|
+
cipher = Enc.for(header['enc'], cek, iv, tag)
|
46
|
+
|
47
|
+
plaintext = cipher.decrypt(ciphertext, payload.split('.').first)
|
48
|
+
|
49
|
+
apply_zip(header, plaintext, :decompress)
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_params(header, key)
|
53
|
+
check_alg(header[:alg] || header['alg'])
|
54
|
+
check_enc(header[:enc] || header['enc'])
|
55
|
+
check_zip(header[:zip] || header['zip'])
|
56
|
+
check_key(key)
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_alg(alg)
|
60
|
+
raise ArgumentError.new("\"#{alg}\" is not a valid alg method") unless VALID_ALG.include?(alg)
|
61
|
+
end
|
41
62
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
base64header = payload.split('.').first
|
63
|
+
def check_enc(enc)
|
64
|
+
raise ArgumentError.new("\"#{enc}\" is not a valid enc method") unless VALID_ENC.include?(enc)
|
65
|
+
end
|
46
66
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
raise ArgumentError.new('The key must not be nil or blank') if key.nil? || (key.is_a?(String) && key.strip == '')
|
67
|
+
def check_zip(zip)
|
68
|
+
raise ArgumentError.new("\"#{zip}\" is not a valid zip method") unless zip.nil? || zip == '' || VALID_ZIP.include?(zip)
|
69
|
+
end
|
51
70
|
|
52
|
-
|
53
|
-
|
54
|
-
|
71
|
+
def check_key(key)
|
72
|
+
raise ArgumentError.new('The key must not be nil or blank') if key.nil? || (key.is_a?(String) && key.strip == '')
|
73
|
+
end
|
55
74
|
|
56
|
-
|
75
|
+
def param_to_class_name(param)
|
76
|
+
klass = param.gsub(/[-\+]/, '_').downcase.sub(/^[a-z\d]*/) { $&.capitalize }
|
77
|
+
klass.gsub(/_([a-z\d]*)/i) { Regexp.last_match(1).capitalize }
|
78
|
+
end
|
79
|
+
|
80
|
+
def apply_zip(header, data, direction)
|
81
|
+
zip = header[:zip] || header['zip']
|
82
|
+
if zip
|
83
|
+
Zip.for(zip).new.send(direction, data)
|
84
|
+
else
|
85
|
+
data
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def generate_header(alg, enc, more)
|
90
|
+
header = { alg: alg, enc: enc }.merge(more)
|
91
|
+
header.delete(:zip) if header[:zip] == ''
|
92
|
+
header
|
93
|
+
end
|
57
94
|
|
58
|
-
|
59
|
-
|
60
|
-
else
|
61
|
-
plaintext
|
95
|
+
def generate_serialization(hdr, cek, content, cipher)
|
96
|
+
Serialization::Compact.encode(hdr, cek, cipher.iv, content, cipher.tag)
|
62
97
|
end
|
63
98
|
end
|
64
99
|
end
|
data/lib/jwe/alg.rb
CHANGED
@@ -1,17 +1,25 @@
|
|
1
1
|
require 'jwe/alg/a128_kw'
|
2
|
+
require 'jwe/alg/a192_kw'
|
3
|
+
require 'jwe/alg/a256_kw'
|
2
4
|
require 'jwe/alg/dir'
|
3
5
|
require 'jwe/alg/rsa_oaep'
|
4
6
|
require 'jwe/alg/rsa15'
|
5
7
|
|
6
8
|
module JWE
|
9
|
+
# Key encryption algorithms namespace
|
7
10
|
module Alg
|
8
11
|
def self.for(alg)
|
9
|
-
|
10
|
-
klass.gsub!(/_([a-z\d]*)/i) { Regexp.last_match(1).capitalize }
|
11
|
-
const_get(klass)
|
12
|
-
|
12
|
+
const_get(JWE.param_to_class_name(alg))
|
13
13
|
rescue NameError
|
14
14
|
raise NotImplementedError.new("Unsupported alg type: #{alg}")
|
15
15
|
end
|
16
|
+
|
17
|
+
def self.encrypt_cek(alg, key, cek)
|
18
|
+
self.for(alg).new(key).encrypt(cek)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.decrypt_cek(alg, key, encrypted_cek)
|
22
|
+
self.for(alg).new(key).decrypt(encrypted_cek)
|
23
|
+
end
|
16
24
|
end
|
17
25
|
end
|
data/lib/jwe/alg/a128_kw.rb
CHANGED
data/lib/jwe/alg/a192_kw.rb
CHANGED
data/lib/jwe/alg/a256_kw.rb
CHANGED
data/lib/jwe/alg/aes_kw.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
require 'jwe/enc/cipher'
|
2
|
+
|
1
3
|
module JWE
|
2
4
|
module Alg
|
5
|
+
# Generic AES Key Wrapping algorithm for any key size.
|
3
6
|
module AesKw
|
4
7
|
attr_accessor :key
|
5
8
|
attr_accessor :iv
|
@@ -11,39 +14,33 @@ module JWE
|
|
11
14
|
|
12
15
|
def encrypt(cek)
|
13
16
|
a = iv
|
14
|
-
r = cek.scan(/.{8}/m)
|
17
|
+
r = cek.force_encoding('ASCII-8BIT').scan(/.{8}/m)
|
15
18
|
|
16
19
|
6.times do |j|
|
17
|
-
r
|
18
|
-
b = encrypt_round(a + r[i])
|
19
|
-
|
20
|
-
a = b.chars.first(8).join
|
21
|
-
r[i] = b.chars.last(8).join
|
22
|
-
|
23
|
-
t = (r.length * j) + i + 1
|
24
|
-
a = xor(a, t)
|
25
|
-
end
|
20
|
+
a, r = kw_encrypt_round(j, a, r)
|
26
21
|
end
|
27
22
|
|
28
23
|
([a] + r).join
|
29
24
|
end
|
30
25
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
26
|
+
def kw_encrypt_round(j, a, r)
|
27
|
+
r.length.times do |i|
|
28
|
+
b = encrypt_round(a + r[i]).chars
|
34
29
|
|
35
|
-
|
30
|
+
a, r[i] = a_ri(b)
|
36
31
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
32
|
+
a = xor(a, (r.length * j) + i + 1)
|
33
|
+
end
|
34
|
+
|
35
|
+
[a, r]
|
36
|
+
end
|
41
37
|
|
42
|
-
|
38
|
+
def decrypt(encrypted_cek)
|
39
|
+
c = encrypted_cek.force_encoding('ASCII-8BIT').scan(/.{8}/m)
|
40
|
+
a, *r = c
|
43
41
|
|
44
|
-
|
45
|
-
|
46
|
-
end
|
42
|
+
5.downto(0) do |j|
|
43
|
+
a, r = kw_decrypt_round(j, a, r)
|
47
44
|
end
|
48
45
|
|
49
46
|
if a != iv
|
@@ -53,10 +50,24 @@ module JWE
|
|
53
50
|
r.join
|
54
51
|
end
|
55
52
|
|
53
|
+
def kw_decrypt_round(j, a, r)
|
54
|
+
r.length.downto(1) do |i|
|
55
|
+
a = xor(a, (r.length * j) + i)
|
56
|
+
|
57
|
+
b = decrypt_round(a + r[i - 1]).chars
|
58
|
+
|
59
|
+
a, r[i - 1] = a_ri(b)
|
60
|
+
end
|
61
|
+
|
62
|
+
[a, r]
|
63
|
+
end
|
64
|
+
|
65
|
+
def a_ri(b)
|
66
|
+
[b.first(8).join, b.last(8).join]
|
67
|
+
end
|
68
|
+
|
56
69
|
def cipher
|
57
|
-
@cipher ||=
|
58
|
-
rescue RuntimeError
|
59
|
-
raise JWE::NotImplementedError.new("The version of OpenSSL linked to your Ruby does not support the cipher #{cipher_name}.")
|
70
|
+
@cipher ||= Enc::Cipher.for(cipher_name)
|
60
71
|
end
|
61
72
|
|
62
73
|
def encrypt_round(data)
|
data/lib/jwe/alg/dir.rb
CHANGED
data/lib/jwe/alg/rsa15.rb
CHANGED
data/lib/jwe/alg/rsa_oaep.rb
CHANGED
data/lib/jwe/base64.rb
CHANGED
data/lib/jwe/enc.rb
CHANGED
@@ -6,12 +6,13 @@ require 'jwe/enc/a192gcm'
|
|
6
6
|
require 'jwe/enc/a256gcm'
|
7
7
|
|
8
8
|
module JWE
|
9
|
+
# Content encryption algorithms namespace
|
9
10
|
module Enc
|
10
|
-
def self.for(enc)
|
11
|
-
klass =
|
12
|
-
klass.
|
13
|
-
|
14
|
-
|
11
|
+
def self.for(enc, cek = nil, iv = nil, tag = nil)
|
12
|
+
klass = const_get(JWE.param_to_class_name(enc))
|
13
|
+
inst = klass.new(cek, iv)
|
14
|
+
inst.tag = tag if tag
|
15
|
+
inst
|
15
16
|
rescue NameError
|
16
17
|
raise NotImplementedError.new("Unsupported enc type: #{enc}")
|
17
18
|
end
|
data/lib/jwe/enc/a128gcm.rb
CHANGED
data/lib/jwe/enc/a192gcm.rb
CHANGED
data/lib/jwe/enc/a256gcm.rb
CHANGED
data/lib/jwe/enc/aes_cbc_hs.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
require 'jwe/enc/cipher'
|
2
|
+
|
1
3
|
module JWE
|
2
4
|
module Enc
|
5
|
+
# Abstract AES in Block cipher mode, with message signature for different key sizes.
|
3
6
|
module AesCbcHs
|
4
7
|
attr_accessor :cek
|
5
8
|
attr_accessor :iv
|
@@ -13,37 +16,41 @@ module JWE
|
|
13
16
|
def encrypt(cleartext, authenticated_data)
|
14
17
|
raise JWE::BadCEK.new("The supplied key is invalid. Required length: #{key_length}") if cek.length != key_length
|
15
18
|
|
16
|
-
|
17
|
-
cipher.key = enc_key
|
18
|
-
cipher.iv = iv
|
19
|
+
ciphertext = cipher_round(:encrypt, iv, cleartext)
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
to_sign = authenticated_data + iv + ciphertext + length
|
24
|
-
signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new(hash_name), mac_key, to_sign)
|
25
|
-
self.tag = signature[0...mac_key.length]
|
21
|
+
signature = generate_tag(authenticated_data, iv, ciphertext)
|
22
|
+
self.tag = signature
|
26
23
|
|
27
24
|
ciphertext
|
28
25
|
end
|
29
26
|
|
30
27
|
def decrypt(ciphertext, authenticated_data)
|
31
|
-
raise JWE::BadCEK
|
28
|
+
raise JWE::BadCEK, "The supplied key is invalid. Required length: #{key_length}" if cek.length != key_length
|
32
29
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
if signature[0...mac_key.length] != tag
|
37
|
-
raise JWE::InvalidData.new('Authentication tag verification failed')
|
30
|
+
signature = generate_tag(authenticated_data, iv, ciphertext)
|
31
|
+
if signature != tag
|
32
|
+
raise JWE::InvalidData, 'Authentication tag verification failed'
|
38
33
|
end
|
39
34
|
|
40
|
-
|
35
|
+
cipher_round(:decrypt, iv, ciphertext)
|
36
|
+
rescue OpenSSL::Cipher::CipherError
|
37
|
+
raise JWE::InvalidData, 'Invalid ciphertext or authentication tag'
|
38
|
+
end
|
39
|
+
|
40
|
+
def cipher_round(direction, iv, data)
|
41
|
+
cipher.send(direction)
|
41
42
|
cipher.key = enc_key
|
42
43
|
cipher.iv = iv
|
43
44
|
|
44
|
-
cipher.update(
|
45
|
-
|
46
|
-
|
45
|
+
cipher.update(data) + cipher.final
|
46
|
+
end
|
47
|
+
|
48
|
+
def generate_tag(authenticated_data, iv, ciphertext)
|
49
|
+
length = [authenticated_data.length * 8].pack('Q>') # 64bit big endian
|
50
|
+
to_sign = authenticated_data + iv + ciphertext + length
|
51
|
+
signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new(hash_name), mac_key, to_sign)
|
52
|
+
|
53
|
+
signature[0...mac_key.length]
|
47
54
|
end
|
48
55
|
|
49
56
|
def iv
|
@@ -63,9 +70,7 @@ module JWE
|
|
63
70
|
end
|
64
71
|
|
65
72
|
def cipher
|
66
|
-
@cipher ||=
|
67
|
-
rescue RuntimeError
|
68
|
-
raise JWE::NotImplementedError.new("The version of OpenSSL linked to your Ruby does not support the cipher #{cipher_name}.")
|
73
|
+
@cipher ||= Cipher.for(cipher_name)
|
69
74
|
end
|
70
75
|
|
71
76
|
def tag
|
@@ -76,6 +81,7 @@ module JWE
|
|
76
81
|
base.extend(ClassMethods)
|
77
82
|
end
|
78
83
|
|
84
|
+
# Provides availability checks for Key Encryption algorithms
|
79
85
|
module ClassMethods
|
80
86
|
def available?
|
81
87
|
new.cipher
|
data/lib/jwe/enc/aes_gcm.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
require 'jwe/enc/cipher'
|
2
|
+
|
1
3
|
module JWE
|
2
4
|
module Enc
|
5
|
+
# Abstract AES in Galois Counter mode for different key sizes.
|
3
6
|
module AesGcm
|
4
7
|
attr_accessor :cek
|
5
8
|
attr_accessor :iv
|
@@ -11,13 +14,9 @@ module JWE
|
|
11
14
|
end
|
12
15
|
|
13
16
|
def encrypt(cleartext, authenticated_data)
|
14
|
-
raise JWE::BadCEK
|
15
|
-
|
16
|
-
cipher.encrypt
|
17
|
-
cipher.key = cek
|
18
|
-
cipher.iv = iv
|
19
|
-
cipher.auth_data = authenticated_data
|
17
|
+
raise JWE::BadCEK, "The supplied key is too short. Required length: #{key_length}" if cek.length < key_length
|
20
18
|
|
19
|
+
setup_cipher(:encrypt, authenticated_data)
|
21
20
|
ciphertext = cipher.update(cleartext) + cipher.final
|
22
21
|
self.tag = cipher.auth_tag
|
23
22
|
|
@@ -25,17 +24,20 @@ module JWE
|
|
25
24
|
end
|
26
25
|
|
27
26
|
def decrypt(ciphertext, authenticated_data)
|
28
|
-
raise JWE::BadCEK
|
29
|
-
|
30
|
-
cipher.decrypt
|
31
|
-
cipher.key = cek
|
32
|
-
cipher.iv = iv
|
33
|
-
cipher.auth_tag = tag
|
34
|
-
cipher.auth_data = authenticated_data
|
27
|
+
raise JWE::BadCEK, "The supplied key is too short. Required length: #{key_length}" if cek.length < key_length
|
35
28
|
|
29
|
+
setup_cipher(:decrypt, authenticated_data)
|
36
30
|
cipher.update(ciphertext) + cipher.final
|
37
31
|
rescue OpenSSL::Cipher::CipherError
|
38
|
-
raise JWE::InvalidData
|
32
|
+
raise JWE::InvalidData, 'Invalid ciphertext or authentication tag'
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup_cipher(direction, auth_data)
|
36
|
+
cipher.send(direction)
|
37
|
+
cipher.key = cek
|
38
|
+
cipher.iv = iv
|
39
|
+
cipher.auth_tag = tag if direction == :decrypt
|
40
|
+
cipher.auth_data = auth_data
|
39
41
|
end
|
40
42
|
|
41
43
|
def iv
|
@@ -47,9 +49,7 @@ module JWE
|
|
47
49
|
end
|
48
50
|
|
49
51
|
def cipher
|
50
|
-
@cipher ||=
|
51
|
-
rescue RuntimeError
|
52
|
-
raise JWE::NotImplementedError.new("The version of OpenSSL linked to your Ruby does not support the cipher #{cipher_name}.")
|
52
|
+
@cipher ||= Cipher.for(cipher_name)
|
53
53
|
end
|
54
54
|
|
55
55
|
def tag
|
@@ -60,6 +60,7 @@ module JWE
|
|
60
60
|
base.extend(ClassMethods)
|
61
61
|
end
|
62
62
|
|
63
|
+
# Provides availability checks for Key Encryption algorithms
|
63
64
|
module ClassMethods
|
64
65
|
def available?
|
65
66
|
new.cipher
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module JWE
|
2
|
+
module Enc
|
3
|
+
# Helper to get OpenSSL cipher instance from a string.
|
4
|
+
module Cipher
|
5
|
+
class << self
|
6
|
+
def for(cipher_name)
|
7
|
+
OpenSSL::Cipher.new(cipher_name)
|
8
|
+
rescue RuntimeError
|
9
|
+
raise JWE::NotImplementedError.new("The version of OpenSSL linked to your Ruby does not support the cipher #{cipher_name}.")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module JWE
|
2
|
+
# Serialization namespace.
|
2
3
|
module Serialization
|
4
|
+
# The default and suggested way of serializing JWE messages.
|
3
5
|
class Compact
|
4
6
|
def self.encode(header, encrypted_cek, iv, ciphertext, tag)
|
5
7
|
[header, encrypted_cek, iv, ciphertext, tag].map { |piece| JWE::Base64.jwe_encode(piece) }.join '.'
|
@@ -7,7 +9,7 @@ module JWE
|
|
7
9
|
|
8
10
|
def self.decode(payload)
|
9
11
|
parts = payload.split('.')
|
10
|
-
raise JWE::DecodeError.new('Not
|
12
|
+
raise JWE::DecodeError.new('Not enough or too many segments') unless parts.length == 5
|
11
13
|
|
12
14
|
parts.map do |part|
|
13
15
|
JWE::Base64.jwe_decode(part)
|
data/lib/jwe/version.rb
CHANGED
data/lib/jwe/zip.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
require 'jwe/zip/def'
|
2
2
|
|
3
3
|
module JWE
|
4
|
+
# Message deflating algorithms namespace
|
4
5
|
module Zip
|
5
6
|
def self.for(zip)
|
6
|
-
|
7
|
-
klass.gsub!(/_([a-z\d]*)/i) { Regexp.last_match(1).capitalize }
|
8
|
-
const_get(klass)
|
9
|
-
|
7
|
+
const_get(JWE.param_to_class_name(zip))
|
10
8
|
rescue NameError
|
11
9
|
raise NotImplementedError.new("Unsupported zip type: #{zip}")
|
12
10
|
end
|
data/lib/jwe/zip/def.rb
CHANGED
@@ -2,11 +2,11 @@ require 'zlib'
|
|
2
2
|
|
3
3
|
module JWE
|
4
4
|
module Zip
|
5
|
+
# Deflate algorithm.
|
5
6
|
class Def
|
6
7
|
def compress(payload)
|
7
8
|
zlib = Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS)
|
8
|
-
zlib.deflate(payload)
|
9
|
-
zlib.finish
|
9
|
+
zlib.deflate(payload, Zlib::FINISH)
|
10
10
|
end
|
11
11
|
|
12
12
|
# Was using RFC 1950 instead of 1951.
|
data/spec/jwe/enc_spec.rb
CHANGED
@@ -7,8 +7,8 @@ require 'jwe/enc/a256gcm'
|
|
7
7
|
|
8
8
|
describe JWE::Enc do
|
9
9
|
describe '.for' do
|
10
|
-
it 'returns
|
11
|
-
expect(JWE::Enc.for('A128GCM')).to
|
10
|
+
it 'returns an instance for the specified enc' do
|
11
|
+
expect(JWE::Enc.for('A128GCM')).to be_a JWE::Enc::A128gcm
|
12
12
|
end
|
13
13
|
|
14
14
|
it 'raises an error for a not-implemented enc' do
|
@@ -1,14 +1,14 @@
|
|
1
1
|
describe JWE::Serialization::Compact do
|
2
2
|
describe '#encode' do
|
3
3
|
it 'returns components base64ed and joined with a dot' do
|
4
|
-
components = %w
|
4
|
+
components = %w[a b c d e]
|
5
5
|
expect(JWE::Serialization::Compact.encode(*components)).to eq 'YQ.Yg.Yw.ZA.ZQ'
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
9
|
describe '#decode' do
|
10
10
|
it 'returns an array with the 5 components' do
|
11
|
-
expect(JWE::Serialization::Compact.decode('YQ.Yg.Yw.ZA.ZQ')).to eq %w
|
11
|
+
expect(JWE::Serialization::Compact.decode('YQ.Yg.Yw.ZA.ZQ')).to eq %w[a b c d e]
|
12
12
|
end
|
13
13
|
|
14
14
|
it 'raises an error when passed a badly formatted payload' do
|
data/spec/jwe/zip_spec.rb
CHANGED
@@ -13,10 +13,20 @@ describe JWE::Zip do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
describe JWE::Zip::Def do
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
context 'with the orginal payload' do
|
17
|
+
it 'deflates and inflates to original payload' do
|
18
|
+
deflate = JWE::Zip::Def.new
|
19
|
+
deflated = deflate.compress('hello world')
|
20
|
+
expect(deflate.decompress(deflated)).to eq 'hello world'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'deflates and inflates a large payload' do
|
24
|
+
deflate = JWE::Zip::Def.new
|
25
|
+
chars = [*'0'..'9', *'A'..'Z', *'a'..'z']
|
26
|
+
payload = Array.new(1_000_000) { chars.sample }.join
|
27
|
+
deflated = deflate.compress(payload)
|
28
|
+
expect(deflate.decompress(deflated)).to eq payload
|
29
|
+
end
|
20
30
|
end
|
21
31
|
|
22
32
|
it 'can deflate an RFC 1950 compressed message' do
|
data/spec/jwe_spec.rb
CHANGED
@@ -29,6 +29,18 @@ describe JWE do
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
+
describe 'when using extra headers' do
|
33
|
+
it 'roundtrips' do
|
34
|
+
encrypted = JWE.encrypt(plaintext, rsa_key, kid: 'some-kid-1')
|
35
|
+
result = JWE.decrypt(encrypted, rsa_key)
|
36
|
+
header, = JWE::Serialization::Compact.decode(encrypted)
|
37
|
+
header = JSON.parse(header)
|
38
|
+
|
39
|
+
expect(header['kid']).to eq 'some-kid-1'
|
40
|
+
expect(result).to eq plaintext
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
32
44
|
it 'raises when passed a bad alg' do
|
33
45
|
expect { JWE.encrypt(plaintext, rsa_key, alg: 'TEST') }.to raise_error(ArgumentError)
|
34
46
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jwe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Francesco Boffa
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-05-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: codeclimate-test-reporter
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
description: A Ruby implementation of the RFC 7516 JSON Web Encryption (JWE) standard
|
56
70
|
email: fra.boffa@gmail.com
|
57
71
|
executables: []
|
@@ -87,6 +101,7 @@ files:
|
|
87
101
|
- lib/jwe/enc/a256gcm.rb
|
88
102
|
- lib/jwe/enc/aes_cbc_hs.rb
|
89
103
|
- lib/jwe/enc/aes_gcm.rb
|
104
|
+
- lib/jwe/enc/cipher.rb
|
90
105
|
- lib/jwe/serialization/compact.rb
|
91
106
|
- lib/jwe/version.rb
|
92
107
|
- lib/jwe/zip.rb
|
@@ -119,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
134
|
version: '0'
|
120
135
|
requirements: []
|
121
136
|
rubyforge_project:
|
122
|
-
rubygems_version: 2.6
|
137
|
+
rubygems_version: 2.7.6
|
123
138
|
signing_key:
|
124
139
|
specification_version: 4
|
125
140
|
summary: JSON Web Encryption implementation in Ruby
|