Skip to content
This repository has been archived by the owner on Jan 1, 2024. It is now read-only.

Commit

Permalink
Fixed up the remaining tests to work with draft-07.
Browse files Browse the repository at this point in the history
  • Loading branch information
gregbeech committed May 11, 2013
1 parent 111236e commit 8313eb9
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 106 deletions.
2 changes: 1 addition & 1 deletion lib/sandal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def self.encrypt_token(payload, encrypter, header_fields = nil)
payload = Zlib::Deflate.deflate(payload, Zlib::BEST_COMPRESSION)
end

encrypter.encrypt(header, payload)
encrypter.encrypt(MultiJson.dump(header), payload)
end

# Decodes and validates a signed and/or encrypted JSON Web Token, recursing
Expand Down
28 changes: 18 additions & 10 deletions lib/sandal/enc/acbc_hs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class ACBC_HS
#
# @param aes_size [Integer] The size of the AES algorithm.
# @param sha_size [Integer] The size of the SHA algorithm.
# @param alg [#name, #encrypt_cmk, #decrypt_cmk] The algorithm to use to encrypt and/or decrypt the AES key.
# @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
def initialize(aes_size, sha_size, alg)
@aes_size = aes_size
@sha_size = sha_size
Expand All @@ -31,14 +31,14 @@ def initialize(aes_size, sha_size, alg)
def encrypt(header, payload)
key = get_encryption_key
mac_key, enc_key = derive_keys(key)
encrypted_key = @alg.encrypt_cmk(key)
encrypted_key = @alg.encrypt_key(key)

cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
cipher.key = enc_key
iv = cipher.random_iv
ciphertext = cipher.update(payload) + cipher.final

auth_data = [MultiJson.dump(header), encrypted_key].map { |part| jwt_base64_encode(part) }.join('.')
auth_data = [header, encrypted_key].map { |part| jwt_base64_encode(part) }.join('.')
auth_data_length = [auth_data.length * 8].pack('Q>')
mac_input = [auth_data, iv, ciphertext, auth_data_length].join
mac = OpenSSL::HMAC.digest(@digest, mac_key, mac_input)
Expand All @@ -52,7 +52,7 @@ def decrypt(token)
parts, decoded_parts = Sandal::Enc.token_parts(token)
header, encrypted_key, iv, ciphertext, auth_tag = *decoded_parts

key = @alg.decrypt_cmk(encrypted_key)
key = @alg.decrypt_key(encrypted_key)
mac_key, enc_key = derive_keys(key)

auth_data = parts.take(2).join('.')
Expand All @@ -77,15 +77,15 @@ def decrypt(token)

# Gets the key to use for mac and encryption
def get_encryption_key
key_size = @sha_size / 8
key_bytes = @sha_size / 8
if @alg.respond_to?(:direct_key)
key = @alg.direct_key
unless key.size == key_size
raise Sandal::KeyError, "The direct key must be #{@key_size * 8} bits"
unless key.size == key_bytes
raise Sandal::KeyError, "The pre-shared content key must be #{@sha_size} bits."
end
key
else
SecureRandom.random_bytes(key_size)
SecureRandom.random_bytes(key_bytes)
end
end

Expand All @@ -101,15 +101,23 @@ def derive_keys(key)

# The A128CBC-HS256 encryption method.
class A128CBC_HS256 < Sandal::Enc::ACBC_HS

# The size of key that is required, in bits.
KEY_SIZE = 256

def initialize(key)
super(128, 256, key)
super(KEY_SIZE / 2, KEY_SIZE, key)
end
end

# The A256CBC-HS512 encryption method.
class A256CBC_HS512 < Sandal::Enc::ACBC_HS

# The size of key that is required, in bits.
KEY_SIZE = 512

def initialize(key)
super(256, 512, key)
super(KEY_SIZE / 2, KEY_SIZE, key)
end
end

Expand Down
26 changes: 19 additions & 7 deletions lib/sandal/enc/agcm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,26 @@ class AGCM
# The JWA algorithm used to encrypt the content encryption key.
attr_reader :alg

# The size of key needed for the algorithm, in bits.
attr_reader :key_size

def initialize(aes_size, alg)
@aes_size = aes_size
@key_size = aes_size
@name = "A#{aes_size}GCM"
@cipher_name = "aes-#{aes_size}-gcm"
@alg = alg
end

def encrypt(header, payload)
cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
cmk = @alg.respond_to?(:cmk) ? @alg.cmk : cipher.random_key
encrypted_key = @alg.encrypt_cmk(cmk)
key = @alg.respond_to?(:direct_key) ? @alg.direct_key : cipher.random_key
encrypted_key = @alg.encrypt_key(key)

cipher.key = cmk
cipher.key = key
iv = cipher.random_iv

auth_parts = [MultiJson.dump(header), encrypted_key]
auth_parts = [header, encrypted_key]
auth_data = auth_parts.map { |part| jwt_base64_encode(part) }.join('.')
cipher.auth_data = auth_data

Expand All @@ -43,7 +47,7 @@ def decrypt(token)
parts, decoded_parts = Sandal::Enc.token_parts(token)
cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
begin
cipher.key = @alg.decrypt_cmk(decoded_parts[1])
cipher.key = @alg.decrypt_key(decoded_parts[1])
cipher.iv = decoded_parts[2]
cipher.auth_tag = decoded_parts[4]
cipher.auth_data = parts.take(2).join('.')
Expand All @@ -57,15 +61,23 @@ def decrypt(token)

# The A128GCM encryption method.
class A128GCM < Sandal::Enc::AGCM

# The size of key that is required, in bits.
KEY_SIZE = 128

def initialize(key)
super(128, key)
super(KEY_SIZE, key)
end
end

# The A256GCM encryption method.
class A256GCM < Sandal::Enc::AGCM

# The size of key that is required, in bits.
KEY_SIZE = 256

def initialize(key)
super(256, key)
super(KEY_SIZE, key)
end
end

Expand Down
24 changes: 11 additions & 13 deletions lib/sandal/enc/alg/direct.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,38 @@ module Sandal
module Enc
module Alg

# The direct ("dir") key encryption mechanism, which uses a pre-shared
# content master key.
# The direct ("dir") key encryption algorithm, which uses a pre-shared symmetric key.
class Direct

# @return [String] The JWA name of the algorithm.
attr_reader :name

# @return [String] The pre-shared key.
# @return [String] The pre-shared symmetric key.
attr_reader :direct_key

# Creates a new instance.
#
# @param cmk [String] The pre-shared key.
# @param direct_key [String] The pre-shared symmetric key.
def initialize(direct_key)
@name = 'dir'
@direct_key = direct_key
end

# Returns an empty string as the content master key is not included in
# the JWE token.
# Returns an empty string as the key is not included in JWE tokens using direct key exchange.
#
# @param cmk [String] This parameter is ignored.
# @param key [String] This parameter is ignored.
# @return [String] An empty string.
def encrypt_cmk(cmk)
def encrypt_key(key)
''
end

# Returns the pre-shared content master key.
#
# @param encrypted_cmk [String] The encrypted content master key.
# @return [String] The pre-shared content master key.
# @raise [Sandal::TokenError] encrypted_cmk is not nil or empty.
def decrypt_cmk(encrypted_cmk)
unless encrypted_cmk.nil? || encrypted_cmk.empty?
# @param encrypted_key [String] The encrypted key.
# @return [String] The pre-shared symmetric key.
# @raise [Sandal::InvalidTokenError] encrypted_key is not nil or empty.
def decrypt_key(encrypted_key)
unless encrypted_key.nil? || encrypted_key.empty?
raise Sandal::InvalidTokenError, 'Token must not include encrypted CMK.'
end
@direct_key
Expand Down
25 changes: 12 additions & 13 deletions lib/sandal/enc/alg/rsa1_5.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,39 @@ module Sandal
module Enc
module Alg

# The RSAES-PKCS1-V1_5 key encryption mechanism.
# The RSA1_5 key encryption algorithm.
class RSA1_5

# @return [String] The JWA name of the algorithm.
attr_reader :name

# Creates a new instance.
#
# @param key [OpenSSL::PKey::RSA or String] The key to use for CMK
# encryption (public) or decryption (private). If the value is a String
# then it will be passed to the constructor of the RSA class. This must
# @param rsa_key [OpenSSL::PKey::RSA or String] The RSA key to use for key encryption (public) or decryption
# (private). If the value is a String then it will be passed to the constructor of the RSA class. This must
# be at least 2048 bits to be compliant with the JWA specification.
def initialize(key)
def initialize(rsa_key)
@name = 'RSA1_5'
@key = key.is_a?(String) ? OpenSSL::PKey::RSA.new(key) : key
@rsa_key = rsa_key.is_a?(String) ? OpenSSL::PKey::RSA.new(rsa_key) : rsa_key
end

# Encrypts the content master key.
#
# @param cmk [String] The content master key.
# @param key [String] The content master key.
# @return [String] The encrypted content master key.
def encrypt_cmk(cmk)
@key.public_encrypt(cmk)
def encrypt_key(key)
@rsa_key.public_encrypt(key)
end

# Decrypts the content master key.
#
# @param encrypted_cmk [String] The encrypted content master key.
# @param encrypted_key [String] The encrypted content master key.
# @return [String] The pre-shared content master key.
# @raise [Sandal::TokenError] The content master key can't be decrypted.
def decrypt_cmk(encrypted_cmk)
@key.private_decrypt(encrypted_cmk)
def decrypt_key(encrypted_key)
@rsa_key.private_decrypt(encrypted_key)
rescue
raise Sandal::InvalidTokenError, 'Cannot decrypt content master key.'
raise Sandal::InvalidTokenError, 'Cannot decrypt content key.'
end

end
Expand Down
25 changes: 12 additions & 13 deletions lib/sandal/enc/alg/rsa_oaep.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,40 @@ module Sandal
module Enc
module Alg

# The RSAES with OAEP key encryption mechanism.
# The RSA-OAEP key encryption algorithm.
class RSA_OAEP

# @return [String] The JWA name of the algorithm.
attr_reader :name

# Creates a new instance.
#
# @param key [OpenSSL::PKey::RSA or String] The key to use for CMK
# encryption (public) or decryption (private). If the value is a String
# then it will be passed to the constructor of the RSA class. This must
# @param rsa_key [OpenSSL::PKey::RSA or String] The RSA key to use for key encryption (public) or decryption
# (private). If the value is a String then it will be passed to the constructor of the RSA class. This must
# be at least 2048 bits to be compliant with the JWA specification.
def initialize(key)
def initialize(rsa_key)
@name = 'RSA-OAEP'
@key = key.is_a?(String) ? OpenSSL::PKey::RSA.new(key) : key
@rsa_key = rsa_key.is_a?(String) ? OpenSSL::PKey::RSA.new(rsa_key) : rsa_key
@padding = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
end

# Encrypts the content master key.
#
# @param cmk [String] The content master key.
# @param key [String] The content master key.
# @return [String] The encrypted content master key.
def encrypt_cmk(cmk)
@key.public_encrypt(cmk, @padding)
def encrypt_key(key)
@rsa_key.public_encrypt(key, @padding)
end

# Decrypts the content master key.
#
# @param encrypted_cmk [String] The encrypted content master key.
# @param encrypted_key [String] The encrypted content master key.
# @return [String] The pre-shared content master key.
# @raise [Sandal::TokenError] The content master key can't be decrypted.
def decrypt_cmk(encrypted_cmk)
@key.private_decrypt(encrypted_cmk, @padding)
def decrypt_key(encrypted_key)
@rsa_key.private_decrypt(encrypted_key, @padding)
rescue
raise Sandal::InvalidTokenError, 'Cannot decrypt content master key.'
raise Sandal::InvalidTokenError, 'Cannot decrypt content key.'
end

end
Expand Down
2 changes: 1 addition & 1 deletion spec/sandal/enc/a256gcm_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
it 'raises an InvalidTokenError when the wrong key is used for decryption' do
token = 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.ApfOLCaDbqs_JXPYy2I937v_xmrzj-Iss1mG6NAHmeJViM6j2l0MHvfseIdHVyU2BIoGVu9ohvkkWiRq5DL2jYZTPA9TAdwq3FUIVyoH-Pedf6elHIVFi2KGDEspYMtQARMMSBcS7pslx6flh1Cfh3GBKysztVMEhZ_maFkm4PYVCsJsvq6Ct3fg2CJPOs0X1DHuxZKoIGIqcbeK4XEO5a0h5TAuJObKdfO0dKwfNSSbpu5sFrpRFwV2FTTYoqF4zI46N9-_hMIznlEpftRXhScEJuZ9HG8C8CHB1WRZ_J48PleqdhF4o7fB5J1wFqUXBtbtuGJ_A2Xe6AEhrlzCOw.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.ghEgxninkHEAMp4xZtB2mA'
enc = Sandal::Enc::A256GCM.new(Sandal::Enc::Alg::RSA_OAEP.new(OpenSSL::PKey::RSA.new(2048)))
expect { enc.decrypt(token) }.to raise_error Sandal::InvalidTokenError, 'Cannot decrypt content master key.'
expect { enc.decrypt(token) }.to raise_error Sandal::InvalidTokenError, 'Cannot decrypt content key.'
end

it 'raises an InvalidTokenError when the token has an invalid format' do
Expand Down
36 changes: 18 additions & 18 deletions spec/sandal/enc/alg/direct_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,48 @@
context '#name' do

it 'is "dir"' do
alg = Sandal::Enc::Alg::Direct.new('some cmk')
alg = Sandal::Enc::Alg::Direct.new('some key')
alg.name.should == 'dir'
end

end

context '#cmk' do
context '#direct_key' do

it 'returns the real CMK' do
cmk = 'the real cmk'
alg = Sandal::Enc::Alg::Direct.new(cmk)
alg.cmk.should == cmk
it 'returns the real key' do
key = 'the real key'
alg = Sandal::Enc::Alg::Direct.new(key)
alg.direct_key.should == key
end

end

context '#encrypt_cmk' do
context '#encrypt_key' do

it 'returns an empty string' do
alg = Sandal::Enc::Alg::Direct.new('the real cmk')
alg.encrypt_cmk('any value').should == ''
alg = Sandal::Enc::Alg::Direct.new('the real key')
alg.encrypt_key('any value').should == ''
end

end

context '#decrypt_cmk' do
context '#decrypt_key' do

it 'returns the real CMK when the value to decrypt is nil' do
cmk = 'the real cmk'
alg = Sandal::Enc::Alg::Direct.new(cmk)
alg.decrypt_cmk(nil).should == cmk
key = 'the real key'
alg = Sandal::Enc::Alg::Direct.new(key)
alg.decrypt_key(nil).should == key
end

it 'returns the real CMK when the value to decrypt is empty' do
cmk = 'the real cmk'
alg = Sandal::Enc::Alg::Direct.new(cmk)
alg.decrypt_cmk('').should == cmk
key = 'the real key'
alg = Sandal::Enc::Alg::Direct.new(key)
alg.decrypt_key('').should == key
end

it 'raises a TokenError if the value to decrypt is not nil or empty' do
alg = Sandal::Enc::Alg::Direct.new('the real cmk')
expect { alg.decrypt_cmk('a value') }.to raise_error Sandal::TokenError
alg = Sandal::Enc::Alg::Direct.new('the real key')
expect { alg.decrypt_key('a value') }.to raise_error Sandal::TokenError
end

end
Expand Down

0 comments on commit 8313eb9

Please sign in to comment.