jwe 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 05c87a9be4e5864d4f31495bd609ff3d4abf1a68
4
- data.tar.gz: dc5edbbac44a91be23673da3f5d0b2beb91a8245
2
+ SHA256:
3
+ metadata.gz: a3be72771de62015739c8c0f4db8b6c39c082ed7a3870fb5dce44870418eb2f1
4
+ data.tar.gz: 65f2fc6f7c1ee46e6524b1e631f0858898348e3a67a2328f522ca449c9a9c0ae
5
5
  SHA512:
6
- metadata.gz: a26d929b4124949bbac9137855a32b42a9ab9165c0025e405e924937e99c9222e88096a1d5e40c93360f4e491df4f43ebe80a4f6d9fe0bba8bc34b9d10a94d99
7
- data.tar.gz: b3e9575add99b0ad449c083a3981d419aaeaf17748591f00e293fcb61e556257b2b371af2c13c7ce8f294cfe72ec39d0f5cac7d6f2b46dd2c2864045410962dc
6
+ metadata.gz: a12f84808b5911b81f4afbf2e3ad615abae6543ead869cf1fac1f27b4270158367312b0e5a05b09b3ebf38780344896fa20cb832c7257386cccf8b6d37bdce86
7
+ data.tar.gz: 4e76467052e21da130ed4b5cd2501d861f2011fc4b91600eb86715f32eee0182a2e65a2ea17f5ef35f38516821394ed925768cc66f6f620dcfa53deeb20ef8c1
@@ -1,5 +1,9 @@
1
1
  Metrics/LineLength:
2
2
  Enabled: false
3
3
  Style/RaiseArgs:
4
- EnforcedStyle: compact
5
-
4
+ Enabled: false
5
+ Metrics/BlockLength:
6
+ Enabled: false
7
+ Style/PercentLiteralDelimiters:
8
+ PreferredDelimiters:
9
+ "%w": "[]"
@@ -1,10 +1,14 @@
1
1
  language: ruby
2
+ dist: trusty
2
3
  rvm:
3
- - 2.0.0
4
- - 2.1.0
5
- - 2.2.0
6
- - 2.3.0
7
- - 2.4.0
8
- addons:
9
- code_climate:
10
- repo_token: b5653aee7f7a47c0d70a89feb535648aa06575497b1eab9e16068c49bf4462c3
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
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  source 'https://rubygems.org'
3
2
 
4
3
  gemspec
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # JWE
2
2
 
3
3
  [![Build Status](https://travis-ci.org/jwt/ruby-jwe.svg)](https://travis-ci.org/jwt/ruby-jwe)
4
- [![Code Climate](https://codeclimate.com/github/aomega08/jwe/badges/gpa.svg)](https://codeclimate.com/github/aomega08/jwe)
5
- [![Test Coverage](https://codeclimate.com/github/aomega08/jwe/badges/coverage.svg)](https://codeclimate.com/github/aomega08/jwe/coverage)
4
+ [![Code Climate](https://codeclimate.com/github/jwt/ruby-jwe/badges/gpa.svg)](https://codeclimate.com/github/jwt/ruby-jwe)
5
+ [![Test Coverage](https://codeclimate.com/github/jwt/ruby-jwe/badges/coverage.svg)](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
@@ -3,4 +3,5 @@ begin
3
3
  RSpec::Core::RakeTask.new(:spec)
4
4
  task default: :spec
5
5
  rescue LoadError
6
+ puts 'Where is RSpec?'
6
7
  end
@@ -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(lib)
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
- def self.encrypt(payload, key, alg: 'RSA-OAEP', enc: 'A128GCM', zip: nil)
23
- raise ArgumentError.new("\"#{alg}\" is not a valid alg method") unless VALID_ALG.include?(alg)
24
- raise ArgumentError.new("\"#{enc}\" is not a valid enc method") unless VALID_ENC.include?(enc)
25
- raise ArgumentError.new("\"#{zip}\" is not a valid zip method") unless zip.nil? || zip == '' || VALID_ZIP.include?(zip)
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
- header = { alg: alg, enc: enc }
29
- header[:zip] = zip if zip && zip != ''
28
+ payload = apply_zip(header, payload, :compress)
30
29
 
31
- cipher = Enc.for(enc).new
32
- cipher.cek = key if alg == 'dir'
30
+ cipher = Enc.for(enc)
31
+ cipher.cek = key if alg == 'dir'
33
32
 
34
- payload = Zip.for(zip).new.compress(payload) if zip && zip != ''
33
+ json_hdr = header.to_json
34
+ ciphertext = cipher.encrypt(payload, Base64.jwe_encode(json_hdr))
35
35
 
36
- ciphertext = cipher.encrypt(payload, Base64.jwe_encode(header.to_json))
37
- encrypted_cek = Alg.for(alg).new(key).encrypt(cipher.cek)
36
+ generate_serialization(json_hdr, Alg.encrypt_cek(alg, key, cipher.cek), ciphertext, cipher)
37
+ end
38
38
 
39
- Serialization::Compact.encode(header.to_json, encrypted_cek, cipher.iv, ciphertext, cipher.tag)
40
- end
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
- def self.decrypt(payload, key)
43
- header, enc_key, iv, ciphertext, tag = Serialization::Compact.decode(payload)
44
- header = JSON.parse(header)
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
- raise ArgumentError.new("\"#{header['alg']}\" is not a valid alg method") unless VALID_ALG.include?(header['alg'])
48
- raise ArgumentError.new("\"#{header['enc']}\" is not a valid enc method") unless VALID_ENC.include?(header['enc'])
49
- raise ArgumentError.new("\"#{header['zip']}\" is not a valid zip method") unless header['zip'].nil? || VALID_ZIP.include?(header['zip'])
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
- cek = Alg.for(header['alg']).new(key).decrypt(enc_key)
53
- cipher = Enc.for(header['enc']).new(cek, iv)
54
- cipher.tag = tag
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
- plaintext = cipher.decrypt(ciphertext, base64header)
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
- if header['zip']
59
- Zip.for(header['zip']).new.decompress(plaintext)
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
@@ -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
- klass = alg.gsub(/[-\+]/, '_').downcase.sub(/^[a-z\d]*/) { $&.capitalize }
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
@@ -2,6 +2,7 @@ require 'jwe/alg/aes_kw'
2
2
 
3
3
  module JWE
4
4
  module Alg
5
+ # AES-128 Key Wrapping algorithm
5
6
  class A128Kw
6
7
  include AesKw
7
8
 
@@ -2,6 +2,7 @@ require 'jwe/alg/aes_kw'
2
2
 
3
3
  module JWE
4
4
  module Alg
5
+ # AES-192 Key Wrapping algorithm
5
6
  class A192Kw
6
7
  include AesKw
7
8
 
@@ -2,6 +2,7 @@ require 'jwe/alg/aes_kw'
2
2
 
3
3
  module JWE
4
4
  module Alg
5
+ # AES-256 Key Wrapping algorithm
5
6
  class A256Kw
6
7
  include AesKw
7
8
 
@@ -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.length.times do |i|
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 decrypt(encrypted_cek)
32
- c = encrypted_cek.scan(/.{8}/m)
33
- a = c[0]
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
- r = c[1..c.length]
30
+ a, r[i] = a_ri(b)
36
31
 
37
- 5.downto(0) do |j|
38
- r.length.downto(1) do |i|
39
- t = (r.length * j) + i
40
- a = xor(a, t)
32
+ a = xor(a, (r.length * j) + i + 1)
33
+ end
34
+
35
+ [a, r]
36
+ end
41
37
 
42
- b = decrypt_round(a + r[i - 1])
38
+ def decrypt(encrypted_cek)
39
+ c = encrypted_cek.force_encoding('ASCII-8BIT').scan(/.{8}/m)
40
+ a, *r = c
43
41
 
44
- a = b.chars.first(8).join
45
- r[i - 1] = b.chars.last(8).join
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 ||= OpenSSL::Cipher.new(cipher_name)
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)
@@ -1,5 +1,6 @@
1
1
  module JWE
2
2
  module Alg
3
+ # Direct (no-op) key encryption algorithm.
3
4
  class Dir
4
5
  attr_accessor :key
5
6
 
@@ -1,5 +1,6 @@
1
1
  module JWE
2
2
  module Alg
3
+ # RSA RSA with PKCS1 v1.5 algorithm.
3
4
  class Rsa15
4
5
  attr_accessor :key
5
6
 
@@ -1,5 +1,6 @@
1
1
  module JWE
2
2
  module Alg
3
+ # RSA-OAEP key encryption algorithm.
3
4
  class RsaOaep
4
5
  attr_accessor :key
5
6
 
@@ -1,4 +1,5 @@
1
1
  module JWE
2
+ # Base64 for JWE is slightly different from what ruby provides.
2
3
  module Base64
3
4
  def self.jwe_encode(payload)
4
5
  ::Base64.urlsafe_encode64(payload).delete('=')
@@ -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 = enc.gsub(/[-\+]/, '_').downcase.sub(/^[a-z\d]*/) { $&.capitalize }
12
- klass.gsub!(/_([a-z\d]*)/i) { Regexp.last_match(1).capitalize }
13
- const_get(klass)
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
@@ -2,6 +2,7 @@ require 'jwe/enc/aes_cbc_hs'
2
2
 
3
3
  module JWE
4
4
  module Enc
5
+ # AES CBC 128 + SHA256 message verification algorithm.
5
6
  class A128cbcHs256
6
7
  include AesCbcHs
7
8
 
@@ -2,6 +2,7 @@ require 'jwe/enc/aes_gcm'
2
2
 
3
3
  module JWE
4
4
  module Enc
5
+ # AES GCM 128 algorithm.
5
6
  class A128gcm
6
7
  include AesGcm
7
8
 
@@ -2,6 +2,7 @@ require 'jwe/enc/aes_cbc_hs'
2
2
 
3
3
  module JWE
4
4
  module Enc
5
+ # AES CBC 192 + SHA384 message verification algorithm.
5
6
  class A192cbcHs384
6
7
  include AesCbcHs
7
8
 
@@ -2,6 +2,7 @@ require 'jwe/enc/aes_gcm'
2
2
 
3
3
  module JWE
4
4
  module Enc
5
+ # AES GCM 192 algorithm.
5
6
  class A192gcm
6
7
  include AesGcm
7
8
 
@@ -2,6 +2,7 @@ require 'jwe/enc/aes_cbc_hs'
2
2
 
3
3
  module JWE
4
4
  module Enc
5
+ # AES CBC 256 + SHA512 message verification algorithm.
5
6
  class A256cbcHs512
6
7
  include AesCbcHs
7
8
 
@@ -2,6 +2,7 @@ require 'jwe/enc/aes_gcm'
2
2
 
3
3
  module JWE
4
4
  module Enc
5
+ # AES GCM 256 algorithm.
5
6
  class A256gcm
6
7
  include AesGcm
7
8
 
@@ -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
- cipher.encrypt
17
- cipher.key = enc_key
18
- cipher.iv = iv
19
+ ciphertext = cipher_round(:encrypt, iv, cleartext)
19
20
 
20
- ciphertext = cipher.update(cleartext) + cipher.final
21
- length = [authenticated_data.length * 8].pack('Q>') # 64bit big endian
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.new("The supplied key is invalid. Required length: #{key_length}") if cek.length != key_length
28
+ raise JWE::BadCEK, "The supplied key is invalid. Required length: #{key_length}" if cek.length != key_length
32
29
 
33
- length = [authenticated_data.length * 8].pack('Q>') # 64bit big endian
34
- to_sign = authenticated_data + iv + ciphertext + length
35
- signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new(hash_name), mac_key, to_sign)
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
- cipher.decrypt
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(ciphertext) + cipher.final
45
- rescue OpenSSL::Cipher::CipherError
46
- raise JWE::InvalidData.new('Invalid ciphertext or authentication tag')
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 ||= OpenSSL::Cipher.new(cipher_name)
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
@@ -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.new("The supplied key is too short. Required length: #{key_length}") if cek.length < key_length
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.new("The supplied key is too short. Required length: #{key_length}") if cek.length < key_length
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.new('Invalid ciphertext or authentication tag')
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 ||= OpenSSL::Cipher.new(cipher_name)
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 enaugh or too many segments') unless parts.length == 5
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)
@@ -1,3 +1,3 @@
1
1
  module JWE
2
- VERSION = '0.3.0'.freeze
2
+ VERSION = '0.3.1'.freeze
3
3
  end
@@ -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
- klass = zip.gsub(/[-\+]/, '_').downcase.sub(/^[a-z\d]*/) { $&.capitalize }
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
@@ -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.
@@ -7,8 +7,8 @@ require 'jwe/enc/a256gcm'
7
7
 
8
8
  describe JWE::Enc do
9
9
  describe '.for' do
10
- it 'returns a class for the specified enc' do
11
- expect(JWE::Enc.for('A128GCM')).to eq JWE::Enc::A128gcm
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(a b c d e)
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(a b c d e)
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
@@ -13,10 +13,20 @@ describe JWE::Zip do
13
13
  end
14
14
 
15
15
  describe JWE::Zip::Def do
16
- it 'deflates and inflates to original payload' do
17
- deflate = JWE::Zip::Def.new
18
- deflated = deflate.compress('hello world')
19
- expect(deflate.decompress(deflated)).to eq 'hello world'
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
@@ -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.0
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: 2017-05-02 00:00:00.000000000 Z
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.11
137
+ rubygems_version: 2.7.6
123
138
  signing_key:
124
139
  specification_version: 4
125
140
  summary: JSON Web Encryption implementation in Ruby