jwe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []