jwe 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cb166f2f458cf97d14cdee1860f05aeaa53c5cfd
4
+ data.tar.gz: bd0dbea752a5d38424cd4b45b1f03e2fe85ae550
5
+ SHA512:
6
+ metadata.gz: 775afd82c19c9501a6534c313cdd50bfda73397b8720d79d924efc3632d86de893b344d37e383d0755f5f5499f33166ae074f41803cc11008e924b8d592708df
7
+ data.tar.gz: ffb94097e58257985a0d6334628a2bfa42217e36020a060ea621c49313364067505241d2b163f76027311c5f4071152b11696d5a095de8a083e90cdcbbb14184
@@ -0,0 +1,13 @@
1
+ engines:
2
+ duplication:
3
+ enabled: true
4
+ config:
5
+ languages:
6
+ - ruby
7
+
8
+ ratings:
9
+ paths:
10
+ - lib/**
11
+
12
+ exclude_paths:
13
+ - spec/**/*
@@ -0,0 +1,11 @@
1
+ tmp
2
+ *.gem
3
+ .rvmrc
4
+ bundle
5
+ .bundle
6
+ .rbenv-version
7
+ .rbx
8
+ /.ruby-gemset
9
+ /.ruby-version
10
+ Gemfile.lock
11
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format doc
3
+ -r spec_helper
@@ -0,0 +1,3 @@
1
+ Metrics/LineLength:
2
+ Enabled: false
3
+
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.0
5
+ - 2.2.0
6
+ addons:
7
+ code_climate:
8
+ repo_token: b5653aee7f7a47c0d70a89feb535648aa06575497b1eab9e16068c49bf4462c3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+ source 'https://rubygems.org'
3
+
4
+ gemspec
@@ -0,0 +1,24 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ * Copyright © 2016 Francesco Boffa
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+
@@ -0,0 +1,7 @@
1
+ begin
2
+ require 'rspec/core/rake_task'
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ task :default => :spec
5
+ rescue LoadError
6
+ end
7
+
@@ -0,0 +1,21 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'jwe/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'jwe'
7
+ s.version = JWE::VERSION
8
+ s.summary = 'JSON Web Encryption implementation in Ruby'
9
+ s.description = 'A Ruby implementation of the RFC 7516 JSON Web Encryption (JWE) standard'
10
+ s.authors = [ 'Francesco Boffa' ]
11
+ s.email = 'fra.boffa@gmail.com'
12
+ s.homepage = 'http://github.com/aomega08/jwe'
13
+ s.license = 'MIT'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.require_paths = %w(lib)
17
+
18
+ s.add_development_dependency 'rspec'
19
+ s.add_development_dependency 'rake'
20
+ s.add_development_dependency 'codeclimate-test-reporter'
21
+ end
@@ -0,0 +1,64 @@
1
+ require 'base64'
2
+ require 'json'
3
+ require 'openssl'
4
+ require 'securerandom'
5
+
6
+ require 'jwe/base64'
7
+ require 'jwe/serialization/compact'
8
+ require 'jwe/alg'
9
+ require 'jwe/enc'
10
+ require 'jwe/zip'
11
+
12
+ module JWE
13
+ class DecodeError < Exception; end
14
+ class NotImplementedError < Exception; end
15
+ class BadCEK < Exception; end
16
+ class InvalidData < Exception; end
17
+
18
+ VALID_ALG = [ 'RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256', 'A128KW' 'A192KW', 'A256KW', 'dir', 'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW', 'A128GCMKW', 'A192GCMKW', 'A256GCMKW', 'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW' ]
19
+ VALID_ENC = [ 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM' ]
20
+ VALID_ZIP = [ 'DEF' ]
21
+
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
+
27
+ header = { alg: alg, enc: enc }
28
+ header[:zip] = zip if zip and zip != ''
29
+
30
+ cipher = Enc.for(enc).new
31
+ cipher.cek = key if alg == 'dir'
32
+
33
+ if zip and zip != ''
34
+ payload = Zip.for(zip).new.compress(payload)
35
+ end
36
+
37
+ ciphertext = cipher.encrypt(payload, Base64::jwe_encode(header.to_json))
38
+ encrypted_cek = Alg.for(alg).new(key).encrypt(cipher.cek)
39
+
40
+ Serialization::Compact.encode(header.to_json, encrypted_cek, cipher.iv, ciphertext, cipher.tag)
41
+ end
42
+
43
+ def self.decrypt(payload, key)
44
+ header, enc_key, iv, ciphertext, tag = Serialization::Compact.decode(payload)
45
+ header = JSON.parse(header)
46
+ base64header = payload.split('.').first
47
+
48
+ raise ArgumentError.new("\"#{header['alg']}\" is not a valid alg method") unless VALID_ALG.include?(header['alg'])
49
+ raise ArgumentError.new("\"#{header['enc']}\" is not a valid enc method") unless VALID_ENC.include?(header['enc'])
50
+ raise ArgumentError.new("\"#{header['zip']}\" is not a valid zip method") unless header['zip'].nil? || VALID_ZIP.include?(header['zip'])
51
+
52
+ cek = Alg.for(header['alg']).new(key).decrypt(enc_key)
53
+ cipher = Enc.for(header['enc']).new(cek, iv)
54
+ cipher.tag = tag
55
+
56
+ plaintext = cipher.decrypt(ciphertext, base64header)
57
+
58
+ if header['zip']
59
+ Zip.for(header['zip']).new.decompress(plaintext)
60
+ else
61
+ plaintext
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,16 @@
1
+ require 'jwe/alg/dir'
2
+ require 'jwe/alg/rsa_oaep'
3
+ require 'jwe/alg/rsa15'
4
+
5
+ module JWE
6
+ module Alg
7
+ def self.for(alg)
8
+ klass = alg.gsub(/[-\+]/, '_').downcase.sub(/^[a-z\d]*/) { $&.capitalize }
9
+ klass.gsub!(/_([a-z\d]*)/i) { $1.capitalize }
10
+ const_get(klass)
11
+
12
+ rescue NameError
13
+ raise NotImplementedError.new("Unsupported alg type: #{alg}")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ module JWE
2
+ module Alg
3
+ class Dir
4
+ attr_accessor :key
5
+
6
+ def initialize(key)
7
+ self.key = key
8
+ end
9
+
10
+ def encrypt(cek)
11
+ ''
12
+ end
13
+
14
+ def decrypt(encrypted_cek)
15
+ key
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module JWE
2
+ module Alg
3
+ class Rsa15
4
+ attr_accessor :key
5
+
6
+ def initialize(key)
7
+ self.key = key
8
+ end
9
+
10
+ def encrypt(cek)
11
+ key.public_encrypt(cek)
12
+ end
13
+
14
+ def decrypt(encrypted_cek)
15
+ key.private_decrypt(encrypted_cek)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module JWE
2
+ module Alg
3
+ class RsaOaep
4
+ attr_accessor :key
5
+
6
+ def initialize(key)
7
+ self.key = key
8
+ end
9
+
10
+ def encrypt(cek)
11
+ key.public_encrypt(cek, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
12
+ end
13
+
14
+ def decrypt(encrypted_cek)
15
+ key.private_decrypt(encrypted_cek, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module JWE
2
+ module Base64
3
+ def self.jwe_encode(payload)
4
+ ::Base64.urlsafe_encode64(payload).gsub('=', '')
5
+ end
6
+
7
+ def self.jwe_decode(payload)
8
+ padlen = 4 - (payload.length % 4)
9
+ if padlen < 4
10
+ pad = "=" * padlen
11
+ payload += pad
12
+ end
13
+ ::Base64.urlsafe_decode64(payload)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ require 'jwe/enc/a128cbc_hs256'
2
+ require 'jwe/enc/a192cbc_hs384'
3
+ require 'jwe/enc/a256cbc_hs512'
4
+ require 'jwe/enc/a128gcm'
5
+ require 'jwe/enc/a192gcm'
6
+ require 'jwe/enc/a256gcm'
7
+
8
+ module JWE
9
+ module Enc
10
+ def self.for(enc)
11
+ klass = enc.gsub(/[-\+]/, '_').downcase.sub(/^[a-z\d]*/) { $&.capitalize }
12
+ klass.gsub!(/_([a-z\d]*)/i) { $1.capitalize }
13
+ const_get(klass)
14
+
15
+ rescue NameError
16
+ raise NotImplementedError.new("Unsupported enc type: #{enc}")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ require 'jwe/enc/aes_cbc_hs'
2
+
3
+ module JWE
4
+ module Enc
5
+ class A128cbcHs256
6
+ include AesCbcHs
7
+
8
+ def key_length
9
+ 32
10
+ end
11
+
12
+ def cipher_name
13
+ 'AES-128-CBC'
14
+ end
15
+
16
+ def hash_name
17
+ 'sha256'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ require 'jwe/enc/aes_gcm'
2
+
3
+ module JWE
4
+ module Enc
5
+ class A128gcm
6
+ include AesGcm
7
+
8
+ def key_length
9
+ 16
10
+ end
11
+
12
+ def cipher_name
13
+ 'aes-128-gcm'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ require 'jwe/enc/aes_cbc_hs'
2
+
3
+ module JWE
4
+ module Enc
5
+ class A192cbcHs384
6
+ include AesCbcHs
7
+
8
+ def key_length
9
+ 48
10
+ end
11
+
12
+ def cipher_name
13
+ 'AES-192-CBC'
14
+ end
15
+
16
+ def hash_name
17
+ 'sha384'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ require 'jwe/enc/aes_gcm'
2
+
3
+ module JWE
4
+ module Enc
5
+ class A192gcm
6
+ include AesGcm
7
+
8
+ def key_length
9
+ 24
10
+ end
11
+
12
+ def cipher_name
13
+ 'aes-192-gcm'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ require 'jwe/enc/aes_cbc_hs'
2
+
3
+ module JWE
4
+ module Enc
5
+ class A256cbcHs512
6
+ include AesCbcHs
7
+
8
+ def key_length
9
+ 64
10
+ end
11
+
12
+ def cipher_name
13
+ 'AES-256-CBC'
14
+ end
15
+
16
+ def hash_name
17
+ 'sha512'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ require 'jwe/enc/aes_gcm'
2
+
3
+ module JWE
4
+ module Enc
5
+ class A256gcm
6
+ include AesGcm
7
+
8
+ def key_length
9
+ 32
10
+ end
11
+
12
+ def cipher_name
13
+ 'aes-256-gcm'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,89 @@
1
+ module JWE
2
+ module Enc
3
+ module AesCbcHs
4
+ attr_accessor :cek
5
+ attr_accessor :iv
6
+ attr_accessor :tag
7
+
8
+ def initialize(cek = nil, iv = nil)
9
+ self.iv = iv
10
+ self.cek = cek
11
+ end
12
+
13
+ def encrypt(cleartext, authenticated_data)
14
+ raise JWE::BadCEK.new("The supplied key is invalid. Required length: #{key_length}") if cek.length != key_length
15
+
16
+ cipher.encrypt
17
+ cipher.key = enc_key
18
+ cipher.iv = iv
19
+
20
+ ciphertext = cipher.update(cleartext) + cipher.final
21
+ length = [ciphertext.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]
26
+
27
+ ciphertext
28
+ end
29
+
30
+ def decrypt(ciphertext, authenticated_data)
31
+ raise JWE::BadCEK.new("The supplied key is invalid. Required length: #{key_length}") if cek.length != key_length
32
+
33
+ length = [ciphertext.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")
38
+ end
39
+
40
+ cipher.decrypt
41
+ cipher.key = enc_key
42
+ cipher.iv = iv
43
+
44
+ cipher.update(ciphertext) + cipher.final
45
+ rescue OpenSSL::Cipher::CipherError
46
+ raise JWE::InvalidData.new("Invalid ciphertext or authentication tag")
47
+ end
48
+
49
+ def iv
50
+ @iv ||= SecureRandom.random_bytes(16)
51
+ end
52
+
53
+ def cek
54
+ @cek ||= SecureRandom.random_bytes(key_length)
55
+ end
56
+
57
+ def mac_key
58
+ cek[0 ... key_length / 2]
59
+ end
60
+
61
+ def enc_key
62
+ cek[key_length / 2 .. -1 ]
63
+ end
64
+
65
+ 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}.")
69
+ end
70
+
71
+ def tag
72
+ @tag || ""
73
+ end
74
+
75
+ def self.included(base)
76
+ base.extend(ClassMethods)
77
+ end
78
+
79
+ module ClassMethods
80
+ def available?
81
+ new.cipher
82
+ true
83
+ rescue JWE::NotImplementedError
84
+ false
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,73 @@
1
+ module JWE
2
+ module Enc
3
+ module AesGcm
4
+ attr_accessor :cek
5
+ attr_accessor :iv
6
+ attr_accessor :tag
7
+
8
+ def initialize(cek = nil, iv = nil)
9
+ self.iv = iv
10
+ self.cek = cek
11
+ end
12
+
13
+ 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
20
+
21
+ ciphertext = cipher.update(cleartext) + cipher.final
22
+ self.tag = cipher.auth_tag
23
+
24
+ ciphertext
25
+ end
26
+
27
+ 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
35
+
36
+ cipher.update(ciphertext) + cipher.final
37
+ rescue OpenSSL::Cipher::CipherError
38
+ raise JWE::InvalidData.new("Invalid ciphertext or authentication tag")
39
+ end
40
+
41
+ def iv
42
+ @iv ||= SecureRandom.random_bytes(12)
43
+ end
44
+
45
+ def cek
46
+ @cek ||= SecureRandom.random_bytes(key_length)
47
+ end
48
+
49
+ 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}.")
53
+ end
54
+
55
+ def tag
56
+ @tag || ""
57
+ end
58
+
59
+ def self.included(base)
60
+ base.extend(ClassMethods)
61
+ end
62
+
63
+ module ClassMethods
64
+ def available?
65
+ new.cipher
66
+ true
67
+ rescue JWE::NotImplementedError
68
+ false
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,18 @@
1
+ module JWE
2
+ module Serialization
3
+ class Compact
4
+ def self.encode(header, encrypted_cek, iv, ciphertext, tag)
5
+ [ header, encrypted_cek, iv, ciphertext, tag ].map { |piece| JWE::Base64::jwe_encode(piece) }.join '.'
6
+ end
7
+
8
+ def self.decode(payload)
9
+ parts = payload.split('.')
10
+ raise JWE::DecodeError.new('Not enaugh or too many segments') unless parts.length == 5
11
+
12
+ parts.map do |part|
13
+ JWE::Base64.jwe_decode(part)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ module JWE
2
+ VERSION = '0.1.0'
3
+ end
4
+
@@ -0,0 +1,14 @@
1
+ require 'jwe/zip/def'
2
+
3
+ module JWE
4
+ module Zip
5
+ def self.for(zip)
6
+ klass = zip.gsub(/[-\+]/, '_').downcase.sub(/^[a-z\d]*/) { $&.capitalize }
7
+ klass.gsub!(/_([a-z\d]*)/i) { $1.capitalize }
8
+ const_get(klass)
9
+
10
+ rescue NameError
11
+ raise NotImplementedError.new("Unsupported zip type: #{zip}")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ require 'zlib'
2
+
3
+ module JWE
4
+ module Zip
5
+ class Def
6
+ def compress(payload)
7
+ Zlib::Deflate.deflate(payload)
8
+ end
9
+
10
+ def decompress(payload)
11
+ Zlib::Inflate.inflate(payload)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,66 @@
1
+ require 'jwe/alg/dir'
2
+ require 'jwe/alg/rsa_oaep'
3
+ require 'jwe/alg/rsa15'
4
+ require 'openssl'
5
+
6
+ describe JWE::Alg do
7
+ describe '.for' do
8
+ it 'returns a class for the specified alg' do
9
+ expect(JWE::Alg.for('RSA-OAEP')).to eq JWE::Alg::RsaOaep
10
+ end
11
+
12
+ it 'raises an error for a not-implemented alg' do
13
+ expect { JWE::Alg.for('ERSA-4096-MAGIC') }.to raise_error(JWE::NotImplementedError)
14
+ end
15
+ end
16
+ end
17
+
18
+ describe JWE::Alg::Dir do
19
+ # The direct encryption method does not Encrypt the CEK.
20
+ # When building the final JWE object, the "Encrypted CEK" part is left blank
21
+
22
+ describe '#encrypt' do
23
+ it 'returns an empty string' do
24
+ expect(JWE::Alg::Dir.new('whatever').encrypt('any')).to eq ''
25
+ end
26
+ end
27
+
28
+ describe '#decrypt' do
29
+ it 'returns the original key' do
30
+ expect(JWE::Alg::Dir.new('whatever').decrypt('any')).to eq 'whatever'
31
+ end
32
+ end
33
+ end
34
+
35
+ key_path = File.dirname(__FILE__) + '/../keys/rsa.pem'
36
+ key = OpenSSL::PKey::RSA.new File.read(key_path)
37
+
38
+ describe JWE::Alg::RsaOaep do
39
+ let(:alg) { JWE::Alg::RsaOaep.new(key) }
40
+
41
+ describe '#encrypt' do
42
+ it 'returns an encrypted string' do
43
+ expect(alg.encrypt('random key')).to_not eq 'random key'
44
+ end
45
+ end
46
+
47
+ it 'decrypts the encrypted key to the original key' do
48
+ ciphertext = alg.encrypt('random key')
49
+ expect(alg.decrypt(ciphertext)).to eq 'random key'
50
+ end
51
+ end
52
+
53
+ describe JWE::Alg::Rsa15 do
54
+ let(:alg) { JWE::Alg::Rsa15.new(key) }
55
+
56
+ describe '#encrypt' do
57
+ it 'returns an encrypted string' do
58
+ expect(alg.encrypt('random key')).to_not eq 'random key'
59
+ end
60
+ end
61
+
62
+ it 'decrypts the encrypted key to the original key' do
63
+ ciphertext = alg.encrypt('random key')
64
+ expect(alg.decrypt(ciphertext)).to eq 'random key'
65
+ end
66
+ end
@@ -0,0 +1,27 @@
1
+ require 'jwe/base64'
2
+
3
+ module JWE
4
+ describe Base64 do
5
+ describe '.jwe_encode' do
6
+ it 'encodes the payload using the urlsafe encoding' do
7
+ # "me?" encodes to "bWU/" in standard encoding
8
+ expect(Base64.jwe_encode("me?")).to_not include '/'
9
+ end
10
+
11
+ it 'strips the standard padding' do
12
+ expect(Base64.jwe_encode("a")).to_not end_with '='
13
+ end
14
+ end
15
+
16
+ describe '.jwe_decode' do
17
+ it 'decodes the payload using the urlsafe encoding' do
18
+ # "me?" encodes to "bWU/" in standard encoding
19
+ expect(Base64.jwe_decode("bWU_")).to eq "me?"
20
+ end
21
+
22
+ it 'fixes the padding' do
23
+ expect(Base64.jwe_decode("YQ")).to eq "a"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,211 @@
1
+ require 'jwe/enc/a128cbc_hs256'
2
+ require 'jwe/enc/a192cbc_hs384'
3
+ require 'jwe/enc/a256cbc_hs512'
4
+ require 'jwe/enc/a128gcm'
5
+ require 'jwe/enc/a192gcm'
6
+ require 'jwe/enc/a256gcm'
7
+
8
+ describe JWE::Enc do
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
12
+ end
13
+
14
+ it 'raises an error for a not-implemented enc' do
15
+ expect { JWE::Enc.for('ERSA-4096-MAGIC') }.to raise_error(JWE::NotImplementedError)
16
+ end
17
+ end
18
+ end
19
+
20
+ gcm = [
21
+ {
22
+ class: JWE::Enc::A128gcm,
23
+ keylen: 16,
24
+ helloworld: "\"\xC6\xE4h\x8AI\x83\x90v\xAF\xE2\x11".force_encoding('BINARY'),
25
+ tag: "\x85|\xF7\xE1\x94\tVG\x84\xE1\xA8\x81\a\xF4\xC60".force_encoding('BINARY'),
26
+ ivlen: 12,
27
+ iv: "\x0" * 12
28
+ },
29
+ {
30
+ class: JWE::Enc::A192gcm,
31
+ keylen: 24,
32
+ helloworld: "\x9F\xA4\xEC\xCCa\x86\tRO\xD7\xE3\x8D".force_encoding('BINARY'),
33
+ tag: "\xF6\xC0\xB8\x91A\xB1\xF0}\xD4u\xD0_\xCD\xA7\x17'".force_encoding('BINARY'),
34
+ ivlen: 12,
35
+ iv: "\x0" * 12
36
+ },
37
+ {
38
+ class: JWE::Enc::A256gcm,
39
+ keylen: 32,
40
+ helloworld: "\xFDq\xDC\xDD\x87\x9DK\x97\x03G\x99\f".force_encoding('BINARY'),
41
+ tag: "\xC6\xF1\r\xDD\x14\x7Fqf,6\x0EK\x7F\x9D\x1D\t".force_encoding('BINARY'),
42
+ ivlen: 12,
43
+ iv: "\x0" * 12
44
+ },
45
+ {
46
+ class: JWE::Enc::A128cbcHs256,
47
+ keylen: 32,
48
+ helloworld: "\a\x02F\xA4m%\xDFH\xB4\xA4.\xBF:\xBF$\xE2".force_encoding('BINARY'),
49
+ tag: "\xD2\xC2\xA5M\xF1e\x00\xDB}\xDB\x15\x9F\xFF\x8A\x7F\x94".force_encoding('BINARY'),
50
+ ivlen: 16,
51
+ iv: "\x0" * 16
52
+ },
53
+ {
54
+ class: JWE::Enc::A192cbcHs384,
55
+ keylen: 48,
56
+ helloworld: "p\xFES\xF0\xB4\xCC]8\x1D\xDE\x8Dt\xE7tMh".force_encoding('BINARY'),
57
+ tag: "\xEA\xF4\xD73M\xC6\x1D\x13\x0E\x9E\xAE%L\xD3\x04#\x80:\xA8}\xD7\x16E{".force_encoding('BINARY'),
58
+ ivlen: 16,
59
+ iv: "\x0" * 16
60
+ },
61
+ {
62
+ class: JWE::Enc::A256cbcHs512,
63
+ keylen: 64,
64
+ helloworld: "c\xFD\\\xB9Z\xB6\xE3\xB7\xEE\xA1\xD8\xDF\xB5\xB2\xF8\xEB".force_encoding('BINARY'),
65
+ tag: "\xD2W\xCAE\xBC\xE9\xC5\xCF\xD5\xE0\x88@j\xE4\xA1-\x16\xDA\x8F5(\x1D\x0E\x15.\xDC\x11\x12\x00`\xCER".force_encoding('BINARY'),
66
+ ivlen: 16,
67
+ iv: "\x0" * 16
68
+ }
69
+ ]
70
+
71
+ gcm.each do |group|
72
+ describe group[:class] do
73
+ let(:klass) { group[:class] }
74
+ let(:key) { 'a' * group[:keylen] }
75
+ let(:plaintext) { 'hello world!' }
76
+
77
+ describe '#encrypt' do
78
+ context 'when an invalid key is used' do
79
+ it 'raises an error' do
80
+ enc = klass.new('small')
81
+ expect { enc.encrypt('plain', 'auth') }.to raise_error(JWE::BadCEK)
82
+ end
83
+ end
84
+
85
+ context 'with a valid key' do
86
+ it 'returns the encrypted payload' do
87
+ enc = klass.new(key, group[:iv])
88
+ expect(enc.encrypt(plaintext, '').force_encoding('BINARY')).to eq group[:helloworld]
89
+ end
90
+
91
+ it 'sets an authentication tag' do
92
+ enc = klass.new(key, group[:iv])
93
+ enc.encrypt(plaintext, '')
94
+ expect(enc.tag).to eq group[:tag]
95
+ end
96
+ end
97
+ end
98
+
99
+ describe '#decrypt' do
100
+ context 'when an invalid key is used' do
101
+ it 'raises an error' do
102
+ enc = klass.new('small')
103
+ expect { enc.decrypt('plain', 'auth') }.to raise_error(JWE::BadCEK)
104
+ end
105
+ end
106
+
107
+ context 'with a valid key' do
108
+ context 'when a valid tag is authenticated' do
109
+ it 'returns the plaintext' do
110
+ enc = klass.new(key, group[:iv])
111
+ enc.tag = group[:tag]
112
+ expect(enc.decrypt(group[:helloworld], '')).to eq plaintext
113
+ end
114
+ end
115
+
116
+ context 'when the tag is not valid' do
117
+ it 'raises an error' do
118
+ enc = klass.new(key, group[:iv])
119
+ enc.tag = "random"
120
+ expect { enc.decrypt(group[:helloworld], '') }.to raise_error(JWE::InvalidData)
121
+ end
122
+ end
123
+
124
+ context 'when the tag is not set' do
125
+ it 'raises an error' do
126
+ enc = klass.new(key, group[:iv])
127
+ expect { enc.decrypt(group[:helloworld], '') }.to raise_error(JWE::InvalidData)
128
+ end
129
+ end
130
+
131
+ context 'when the ciphertext is not valid' do
132
+ it 'raises an error' do
133
+ enc = klass.new(key, group[:iv])
134
+ enc.tag = group[:tag]
135
+ expect { enc.decrypt("random", '') }.to raise_error(JWE::InvalidData)
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ describe '#cipher' do
142
+ context 'when the cipher is not supported by the OpenSSL lib' do
143
+ it 'raises an error' do
144
+ enc = klass.new
145
+ allow(enc).to receive(:cipher_name) { 'bad-cipher-128' }
146
+ expect { enc.cipher }.to raise_error(JWE::NotImplementedError)
147
+ end
148
+ end
149
+
150
+ context 'when the cipher is supported' do
151
+ it 'returns the cipher object' do
152
+ enc = klass.new
153
+ allow(enc).to receive(:cipher_name) { OpenSSL::Cipher.ciphers.first }
154
+ expect(enc.cipher).to be_an OpenSSL::Cipher
155
+ end
156
+ end
157
+ end
158
+
159
+ describe '#cek' do
160
+ context 'when a key is not specified in initialization' do
161
+ it "returns a randomly generated #{group[:keylen]}-bytes key" do
162
+ expect(klass.new.cek.length).to eq group[:keylen]
163
+ end
164
+ end
165
+
166
+ context 'when a cek is given' do
167
+ it 'returns the cek' do
168
+ expect(klass.new('cek').cek).to eq 'cek'
169
+ end
170
+ end
171
+ end
172
+
173
+ describe '#iv' do
174
+ context 'when an iv is not specified in initialization' do
175
+ it "returns a randomly generated #{group[:ivlen]}-bytes iv" do
176
+ expect(klass.new.iv.length).to eq group[:ivlen]
177
+ end
178
+ end
179
+
180
+ context 'when a iv is given' do
181
+ it 'returns the iv' do
182
+ expect(klass.new('cek', 'iv').iv).to eq 'iv'
183
+ end
184
+ end
185
+ end
186
+
187
+ describe '.available?' do
188
+ context 'when the cipher is not available' do
189
+ it 'is false' do
190
+ allow_any_instance_of(klass).to receive(:cipher) { raise JWE::NotImplementedError.new }
191
+ expect(klass.available?).to be_falsey
192
+ end
193
+ end
194
+
195
+ context 'when the cipher is available' do
196
+ it 'is true' do
197
+ allow_any_instance_of(klass).to receive(:cipher)
198
+ expect(klass.available?).to be_truthy
199
+ end
200
+ end
201
+ end
202
+
203
+ describe 'full roundtrip' do
204
+ it 'decrypts the ciphertext to the original plaintext' do
205
+ enc = klass.new
206
+ ciphertext = enc.encrypt(plaintext, '')
207
+ expect(enc.decrypt(ciphertext, '')).to eq plaintext
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,18 @@
1
+ describe JWE::Serialization::Compact do
2
+ describe '#encode' do
3
+ it 'returns components base64ed and joined with a dot' do
4
+ components = [ 'a', 'b', 'c', 'd', 'e' ]
5
+ expect(JWE::Serialization::Compact.encode(*components)).to eq 'YQ.Yg.Yw.ZA.ZQ'
6
+ end
7
+ end
8
+
9
+ describe '#decode' do
10
+ it 'returns an array with the 5 components' do
11
+ expect(JWE::Serialization::Compact.decode('YQ.Yg.Yw.ZA.ZQ')).to eq [ 'a', 'b', 'c', 'd', 'e' ]
12
+ end
13
+
14
+ it 'raises an error when passed a badly formatted payload' do
15
+ expect { JWE::Serialization::Compact.decode('YQ.YQ.Yg.Yw.ZA.ZQ') }.to raise_error(JWE::DecodeError)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ require 'jwe/zip/def'
2
+
3
+ describe JWE::Zip do
4
+ describe '.for' do
5
+ it 'returns a class for the specified zip' do
6
+ expect(JWE::Zip.for('DEF')).to eq JWE::Zip::Def
7
+ end
8
+
9
+ it 'raises an error for a not-implemented zip' do
10
+ expect { JWE::Zip.for('BZIP2+JPG') }.to raise_error(JWE::NotImplementedError)
11
+ end
12
+ end
13
+ end
14
+
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"
20
+ end
21
+ end
@@ -0,0 +1,60 @@
1
+ describe JWE do
2
+ let(:plaintext) { "The true sign of intelligence is not knowledge but imagination." }
3
+ let(:rsa_key) { OpenSSL::PKey::RSA.new File.read(File.dirname(__FILE__) + '/keys/rsa.pem') }
4
+ let(:password) { SecureRandom.random_bytes(64) }
5
+
6
+ it 'roundtrips' do
7
+ encrypted = JWE.encrypt(plaintext, rsa_key)
8
+ result = JWE.decrypt(encrypted, rsa_key)
9
+
10
+ expect(result).to eq plaintext
11
+ end
12
+
13
+ describe 'when using DEF compression' do
14
+ it 'roundtrips' do
15
+ encrypted = JWE.encrypt(plaintext, rsa_key, zip: 'DEF')
16
+ result = JWE.decrypt(encrypted, rsa_key)
17
+
18
+ expect(result).to eq plaintext
19
+ end
20
+ end
21
+
22
+ describe 'when using dir alg method' do
23
+ it 'roundtrips' do
24
+ encrypted = JWE.encrypt(plaintext, password, alg: 'dir')
25
+ result = JWE.decrypt(encrypted, password)
26
+
27
+ expect(result).to eq plaintext
28
+ end
29
+ end
30
+
31
+ it 'raises when passed a bad alg' do
32
+ expect { JWE.encrypt(plaintext, rsa_key, alg: 'TEST') }.to raise_error(ArgumentError)
33
+ end
34
+
35
+ it 'raises when passed a bad enc' do
36
+ expect { JWE.encrypt(plaintext, rsa_key, enc: 'TEST') }.to raise_error(ArgumentError)
37
+ end
38
+
39
+ it 'raises when passed a bad zip' do
40
+ expect { JWE.encrypt(plaintext, rsa_key, zip: 'TEST') }.to raise_error(ArgumentError)
41
+ end
42
+
43
+ it 'raises when decoding a bad alg' do
44
+ hdr = { alg: 'TEST', enc: 'A128GCM' }
45
+ payload = JWE::Base64.jwe_encode(hdr.to_json) + ".QY.QY.QY.QY"
46
+ expect { JWE.decrypt(payload, rsa_key) }.to raise_error(ArgumentError)
47
+ end
48
+
49
+ it 'raises when decoding a bad enc' do
50
+ hdr = { alg: 'A192CBC-HS384', enc: 'TEST' }
51
+ payload = JWE::Base64.jwe_encode(hdr.to_json) + ".QY.QY.QY.QY"
52
+ expect { JWE.decrypt(payload, rsa_key) }.to raise_error(ArgumentError)
53
+ end
54
+
55
+ it 'raises when decoding a bad zip' do
56
+ hdr = { alg: 'A192CBC-HS384', enc: 'A128GCM', zip: 'TEST' }
57
+ payload = JWE::Base64.jwe_encode(hdr.to_json) + ".QY.QY.QY.QY"
58
+ expect { JWE.decrypt(payload, rsa_key) }.to raise_error(ArgumentError)
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEogIBAAKCAQEAsqm+NfpjE/i27FvgfOZmoQsC8WcokDRT7pJwK6fVL8nPs1KF
3
+ 0YYHgtQMtsgh2KR1Z+y6cFiiXfzbksMP7XWn5h3G9uZVzaUAz3LM07TUqSA+9dkx
4
+ /QA9Q3VWP5iNBgo59E7LqkAKIE0wfx/rzH85VUCZLFBW5tjcaxzRWyCI9RcpPwmp
5
+ LtmNqoOxdhoy4O7r1mNTrcjlh+l/4I/yavS0+TXeImvOJkIbhIJhhbjE+GDiLEH4
6
+ GxE8j2SThs1nxJtboO1MMZr9hhoHL4Z5qRu6/t+ckO9ONYwUu8eDwQCOsluXsAoe
7
+ CdprYk92M/bvfhtV/C37AUZ6iZUFCS0/FE/VrQIDAQABAoIBAGL+jKdqAmX5hJm4
8
+ Ws25+Bm5eTr7Ns2YQP1K5J4703M0NkKdMgqjYhwKlLTedWqNzYP09mTzp5u+VIeg
9
+ T336mDp4O1toyxg0GhvX90hCxSak+F3Op9UQweFT7aM1SsaS+gO1eUHvU+0L+Bgo
10
+ PsZDpCfpsDWOmmg0twUepZ4BjAGIk8wBPA+cWi8Vmbvnwrwo1643LvtA3p76qUwJ
11
+ EPllQMmEnJ6gUNxQVgqQt/QM0UKPtZ5FyOK3zPcztY5xVO3SJCVvcKkciDo2M7wp
12
+ x9qkRrnBYmouNaQjZJZLHKngHr0DF16sw3ajk8qBZW3d2O72loiOxzAhMmnzm68f
13
+ dDHGNd0CgYEA2JueNMglqYWwOBQipboSMrDprSVR2We19Ji3VS1nXV+tvLsMyspq
14
+ YinH0SkW/xbLnpsOr9yC9jkW0KwFFqPK7TXnrXTVs3a4nAAaDyG8ciZQB2nKSRHt
15
+ H8DrM4IvIa1wU+mxj/Kdkp1L8dD6LLoLpmMsnxpjcvATImuZd5KqInsCgYEA0yeR
16
+ frrx8fOMo40WVVpinxxqycIHIBeI/jHeiwU8kGilQJcrWw3VxpoJriuLsNW2HWR+
17
+ nYz3Th6/FrJv6qhzwTFkgoqRS2Tw7qP+4gxk75hUR0M2a5d4Z0fjltQHaoP+P0kj
18
+ 5iPgshDFDmRcMEPUFx+KtI+g59bTlo6U0gmJY/cCgYBiy/gJExE6lSOfMG/tL0WF
19
+ oXOz6cW/Z7JycgWM8DypNi7EWnynMlP7mhrtp9Q5XWhaW1cDl4yUSc3CN/PKM8Mn
20
+ FuMpFpUyWgAyB0nbhQOy/Q6bkwEU+vww84lT4RkmPzlwzLKUeZCtgtlU3oB9Tg5q
21
+ QenkV+DsV9wiYvmItHitaQKBgFNbe4ScKIdrrkmimP55ABXwEfg0MLvqjppK9Z/M
22
+ IWyg4xvskaEQhSQyC0BG0I6uz4Yq9hEcZUThvm4nYycv+QJ7jUI7kcBByRtsgmKa
23
+ of40FJFNZ15yHYYoSyBv872I/gXdyd5Aq6OgGyrjU8F6BXBbc1Z0nQDpPf5hqz5/
24
+ pU1hAoGAWLjOMTJCFoOOxtdZ39oJmSDN0hImu+KtYWAxa4BmBDimLxwa1Xn4qEsn
25
+ THDodsfMJc7HxYfeyzFZoqjf7vm2Et9eI+/PjT1CQx3DTYxsjk+8BaMqG6p/49Qr
26
+ //JbxdDS735BW/A5rU4TEiJfcV66lT7gI8lL8cFsV1rYPMJWkqc=
27
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,9 @@
1
+ require 'codeclimate-test-reporter'
2
+ CodeClimate::TestReporter.start
3
+
4
+ require 'rspec'
5
+ require 'jwe'
6
+
7
+ RSpec.configure do |config|
8
+ config.order = 'random'
9
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jwe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Francesco Boffa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: codeclimate-test-reporter
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A Ruby implementation of the RFC 7516 JSON Web Encryption (JWE) standard
56
+ email: fra.boffa@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - ".codeclimate.yml"
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".rubocop.yml"
65
+ - ".travis.yml"
66
+ - Gemfile
67
+ - LICENSE.md
68
+ - Rakefile
69
+ - jwe.gemspec
70
+ - lib/jwe.rb
71
+ - lib/jwe/alg.rb
72
+ - lib/jwe/alg/dir.rb
73
+ - lib/jwe/alg/rsa15.rb
74
+ - lib/jwe/alg/rsa_oaep.rb
75
+ - lib/jwe/base64.rb
76
+ - lib/jwe/enc.rb
77
+ - lib/jwe/enc/a128cbc_hs256.rb
78
+ - lib/jwe/enc/a128gcm.rb
79
+ - lib/jwe/enc/a192cbc_hs384.rb
80
+ - lib/jwe/enc/a192gcm.rb
81
+ - lib/jwe/enc/a256cbc_hs512.rb
82
+ - lib/jwe/enc/a256gcm.rb
83
+ - lib/jwe/enc/aes_cbc_hs.rb
84
+ - lib/jwe/enc/aes_gcm.rb
85
+ - lib/jwe/serialization/compact.rb
86
+ - lib/jwe/version.rb
87
+ - lib/jwe/zip.rb
88
+ - lib/jwe/zip/def.rb
89
+ - spec/jwe/alg_spec.rb
90
+ - spec/jwe/base64_spec.rb
91
+ - spec/jwe/enc_spec.rb
92
+ - spec/jwe/serialization_spec.rb
93
+ - spec/jwe/zip_spec.rb
94
+ - spec/jwe_spec.rb
95
+ - spec/keys/rsa.pem
96
+ - spec/spec_helper.rb
97
+ homepage: http://github.com/aomega08/jwe
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.4.5.1
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: JSON Web Encryption implementation in Ruby
121
+ test_files: []