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

Commit

Permalink
Started update to draft-08 spec. Added many more signing tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
gregbeech committed Jun 6, 2013
1 parent 8313eb9 commit e078bfe
Show file tree
Hide file tree
Showing 19 changed files with 455 additions and 486 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Features:

- Updated to JWT draft-07 specification, and corresponding JWE, JWS and JWA drafts.
- Updated to JWT draft-08 specification, and corresponding JWE, JWS and JWA drafts.
- Added a KeyError class for when invalid keys are given to the library.
- Added an ExpiredTokenError class to make handling the common case of expired tokens easier.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sandal [![Build Status](https://travis-ci.org/gregbeech/sandal.png?branch=master)](https://travis-ci.org/gregbeech/sandal) [![Coverage Status](https://coveralls.io/repos/gregbeech/sandal/badge.png?branch=master)](https://coveralls.io/r/gregbeech/sandal) [![Code Climate](https://codeclimate.com/github/gregbeech/sandal.png)](https://codeclimate.com/github/gregbeech/sandal) [![Dependency Status](https://gemnasium.com/gregbeech/sandal.png)](https://gemnasium.com/gregbeech/sandal)

A Ruby library for creating and reading [JSON Web Tokens (JWT) draft-06](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06), supporting [JSON Web Signatures (JWS) draft-08](http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-08) and [JSON Web Encryption (JWE) draft-08](http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-08). See the [CHANGELOG](CHANGELOG.md) for version history.
A Ruby library for creating and reading [JSON Web Tokens (JWT) draft-08](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08), supporting [JSON Web Signatures (JWS) draft-11](http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-11) and [JSON Web Encryption (JWE) draft-11](http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-11). See the [CHANGELOG](CHANGELOG.md) for version history.

## Installation

Expand Down
80 changes: 34 additions & 46 deletions lib/sandal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@
require 'sandal/util'


# A library for creating and reading JSON Web Tokens (JWT), supporting JSON Web
# Signatures (JWS) and JSON Web Encryption (JWE).
# A library for creating and reading JSON Web Tokens (JWT), supporting JSON Web Signatures (JWS) and JSON Web Encryption
# (JWE).
#
# Currently supports draft-06 of the JWT spec, and draft-08 of the JWS and JWE
# specs.
# Currently supports draft-07 of the JWT spec, and draft-10 of the JWS and JWE specs.
module Sandal
extend Sandal::Util

# The base error for all errors raised by this library.
class Error < StandardError; end

# The error that is raised when a key provided for signing/encryption/etc. is invalid.
class KeyError < StandardError; end
class KeyError < Error; end

# The error that is raised when there is a problem with a token.
class TokenError < StandardError; end
class TokenError < Error; end

# The error that is raised when a token is invalid.
class InvalidTokenError < TokenError; end
Expand All @@ -38,24 +40,21 @@ class UnsupportedTokenError < TokenError; end
# The default options for token handling.
#
# ignore_exp::
# Whether to ignore the expiry date of the token. This setting is just to
# help get things working and should always be false in real apps!
# Whether to ignore the expiry date of the token. This setting is just to help get things working and should always
# be false in real apps!
# ignore_nbf::
# Whether to ignore the not-before date of the token. This setting is just
# to help get things working and should always be false in real apps!
# Whether to ignore the not-before date of the token. This setting is just to help get things working and should
# always be false in real apps!
# ignore_signature::
# Whether to ignore the signature of signed (JWS) tokens. This setting is
# just tohelp get things working and should always be false in real apps!
# Whether to ignore the signature of signed (JWS) tokens. This setting is just tohelp get things working and should
# always be false in real apps!
# max_clock_skew::
# The maximum clock skew, in seconds, when validating times. If your server
# time is out of sync with the token server then this can be increased to
# take that into account. It probably shouldn't be more than about 300.
# The maximum clock skew, in seconds, when validating times. If your server time is out of sync with the token
# server then this can be increased to take that into account. It probably shouldn't be more than about 300.
# valid_iss::
# A list of valid token issuers, if validation of the issuer claim is
# required.
# A list of valid token issuers, if validation of the issuer claim is required.
# valid_aud::
# A list of valid audiences, if validation of the audience claim is
# required.
# A list of valid audiences, if validation of the audience claim is required.
DEFAULT_OPTIONS = {
ignore_exp: false,
ignore_nbf: false,
Expand All @@ -67,8 +66,7 @@ class UnsupportedTokenError < TokenError; end

# Overrides the default options.
#
# @param defaults [Hash] The options to override (see {DEFAULT_OPTIONS} for
# details).
# @param defaults [Hash] The options to override (see {DEFAULT_OPTIONS} for details).
# @return [Hash] The new default options.
def self.default!(defaults)
DEFAULT_OPTIONS.merge!(defaults)
Expand Down Expand Up @@ -100,12 +98,9 @@ def self.is_signed?(token)

# Creates a signed JSON Web Token.
#
# @param payload [String or Hash] The payload of the token. Hashes will be
# encoded as JSON.
# @param signer [#name,#sign] The token signer, which may be nil for an
# unsigned token.
# @param header_fields [Hash] Header fields for the token (note: do not
# include 'alg').
# @param payload [String or Hash] The payload of the token. Hashes will be encoded as JSON.
# @param signer [#name,#sign] The token signer, which may be nil for an unsigned token.
# @param header_fields [Hash] Header fields for the token (note: do not include 'alg').
# @return [String] A signed JSON Web Token.
def self.encode_token(payload, signer, header_fields = nil)
signer ||= Sandal::Sig::NONE
Expand All @@ -126,8 +121,7 @@ def self.encode_token(payload, signer, header_fields = nil)
#
# @param payload [String] The payload of the token.
# @param encrypter [#name,#alg,#encrypt] The token encrypter.
# @param header_fields [Hash] Header fields for the token (note: do not
# include 'alg' or 'enc').
# @param header_fields [Hash] Header fields for the token (note: do not include 'alg' or 'enc').
# @return [String] An encrypted JSON Web Token.
def self.encrypt_token(payload, encrypter, header_fields = nil)
header = {}
Expand All @@ -145,28 +139,22 @@ def self.encrypt_token(payload, encrypter, header_fields = nil)
encrypter.encrypt(MultiJson.dump(header), payload)
end

# Decodes and validates a signed and/or encrypted JSON Web Token, recursing
# into any nested tokens, and returns the payload.
# Decodes and validates a signed and/or encrypted JSON Web Token, recursing into any nested tokens, and returns the
# payload.
#
# The block is called with the token header as the first parameter, and should
# return the appropriate signature or decryption method to either validate the
# signature or decrypt the token as applicable. When the tokens are nested,
# this block will be called once per token. It can optionally have a second
# options parameter which can be used to override the {DEFAULT_OPTIONS} on a
# per-token basis; options are not persisted between yields.
# The block is called with the token header as the first parameter, and should return the appropriate signature or
# decryption method to either validate the signature or decrypt the token as applicable. When the tokens are nested,
# this block will be called once per token. It can optionally have a second options parameter which can be used to
# override the {DEFAULT_OPTIONS} on a per-token basis; options are not persisted between yields.
#
# @param token [String] The encoded JSON Web Token.
# @param depth [Integer] The maximum depth of token nesting to decode to.
# @yieldparam header [Hash] The JWT header values.
# @yieldparam options [Hash] (Optional) A hash that can be used to override
# the default options.
# @yieldreturn [#valid? or #decrypt] The signature validator if the token is
# signed, or the token decrypter if the token is encrypted.
# @return [Hash or String] The payload of the token as a Hash if it was JSON,
# otherwise as a String.
# @raise [Sandal::ClaimError] One or more claims in the token is invalid.
# @raise [Sandal::TokenError] The token format is invalid, or validation of
# the token failed.
# @yieldparam options [Hash] (Optional) A hash that can be used to override the default options.
# @yieldreturn [#valid? or #decrypt] The signature validator if the token is signed, or the token decrypter if the
# token is encrypted.
# @return [Hash or String] The payload of the token as a Hash if it was JSON, otherwise as a String.
# @raise [Sandal::TokenError] The token is invalid or not supported.
def self.decode_token(token, depth = 16)
parts = token.split('.')
decoded_parts = decode_token_parts(parts)
Expand Down
53 changes: 36 additions & 17 deletions lib/sandal/enc/acbc_hs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ module Enc
class ACBC_HS
include Sandal::Util

@@iv_size = 128

# The JWA name of the encryption method.
attr_reader :name

# The JWA algorithm used to encrypt the content master key.
attr_reader :alg

# Creates a new instance; it's probably easier to use one of the subclass constructors.
# Initialises a new instance; it's probably easier to use one of the subclass constructors.
#
# @param aes_size [Integer] The size of the AES algorithm.
# @param sha_size [Integer] The size of the SHA algorithm.
# @param aes_size [Integer] The size of the AES algorithm, in bits.
# @param sha_size [Integer] The size of the SHA algorithm, in bits.
# @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
Expand All @@ -28,26 +30,35 @@ def initialize(aes_size, sha_size, alg)
@digest = OpenSSL::Digest.new("sha#{@sha_size}")
end

# Encrypts a token payload.
#
# @param header [String] The header string.
# @param payload [String] The payload.
# @return [String] An encrypted JSON Web Token.
def encrypt(header, payload)
key = get_encryption_key
mac_key, enc_key = derive_keys(key)
encrypted_key = @alg.encrypt_key(key)

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

auth_data = [header, encrypted_key].map { |part| jwt_base64_encode(part) }.join('.')
auth_data_length = [auth_data.length * 8].pack('Q>')
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)
auth_tag = mac[0...(mac.length / 2)]

remainder = [iv, ciphertext, auth_tag].map { |part| jwt_base64_encode(part) }.join('.')
[auth_data, remainder].join('.')
remainder = [iv, ciphertext, auth_tag].map { |part| jwt_base64_encode(part) }.join(".")
[auth_data, remainder].join(".")
end

# Decrypts an encrypted JSON Web Token.
#
# @param token [String or Array] The token, or token parts, to decrypt.
# @return [String] The token payload.
def decrypt(token)
parts, decoded_parts = Sandal::Enc.token_parts(token)
header, encrypted_key, iv, ciphertext, auth_tag = *decoded_parts
Expand All @@ -60,16 +71,16 @@ def decrypt(token)
mac_input = [auth_data, iv, ciphertext, auth_data_length].join
mac = OpenSSL::HMAC.digest(@digest, mac_key, mac_input)
unless auth_tag == mac[0...(mac.length / 2)]
raise Sandal::InvalidTokenError, 'Invalid integrity value.'
raise Sandal::InvalidTokenError, "Invalid authentication tag."
end

cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
begin
cipher.key = enc_key
cipher.iv = decoded_parts[2]
cipher.update(decoded_parts[3]) + cipher.final
rescue OpenSSL::Cipher::CipherError
raise Sandal::InvalidTokenError, 'Cannot decrypt token.'
rescue OpenSSL::Cipher::CipherError => e
raise Sandal::InvalidTokenError, "Cannot decrypt token: #{e.message}"
end
end

Expand All @@ -78,8 +89,8 @@ def decrypt(token)
# Gets the key to use for mac and encryption
def get_encryption_key
key_bytes = @sha_size / 8
if @alg.respond_to?(:direct_key)
key = @alg.direct_key
if @alg.respond_to?(:preshared_key)
key = @alg.preshared_key
unless key.size == key_bytes
raise Sandal::KeyError, "The pre-shared content key must be #{@sha_size} bits."
end
Expand All @@ -105,9 +116,13 @@ class A128CBC_HS256 < Sandal::Enc::ACBC_HS
# The size of key that is required, in bits.
KEY_SIZE = 256

def initialize(key)
super(KEY_SIZE / 2, KEY_SIZE, key)
# Initialises a new instance.
#
# @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
def initialize(alg)
super(KEY_SIZE / 2, KEY_SIZE, alg)
end

end

# The A256CBC-HS512 encryption method.
Expand All @@ -116,9 +131,13 @@ class A256CBC_HS512 < Sandal::Enc::ACBC_HS
# The size of key that is required, in bits.
KEY_SIZE = 512

def initialize(key)
super(KEY_SIZE / 2, KEY_SIZE, key)
# Initialises a new instance.
#
# @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
def initialize(alg)
super(KEY_SIZE / 2, KEY_SIZE, alg)
end

end

end
Expand Down
50 changes: 35 additions & 15 deletions lib/sandal/enc/agcm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,53 @@ module Enc
class AGCM
include Sandal::Util

@@iv_size = 96
@@auth_tag_size = 128

# The JWA name of the encryption method.
attr_reader :name

# 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

# Initialises a new instance; it's probably easier to use one of the subclass constructors.
#
# @param aes_size [Integer] The size of the AES algorithm, in bits.
# @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
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

# Encrypts a token payload.
#
# @param header [String] The header string.
# @param payload [String] The payload.
# @return [String] An encrypted JSON Web Token.
def encrypt(header, payload)
cipher = OpenSSL::Cipher.new(@cipher_name).encrypt
key = @alg.respond_to?(:direct_key) ? @alg.direct_key : cipher.random_key
key = @alg.respond_to?(:preshared_key) ? @alg.preshared_key : cipher.random_key
encrypted_key = @alg.encrypt_key(key)

cipher.key = key
iv = cipher.random_iv
cipher.iv = iv = SecureRandom.random_bytes(@@iv_size / 8)

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

ciphertext = cipher.update(payload) + cipher.final
remaining_parts = [iv, ciphertext, cipher.auth_tag]
remaining_parts = [iv, ciphertext, cipher.auth_tag(@@auth_tag_size / 8)]
remaining_parts.map! { |part| jwt_base64_encode(part) }
[auth_data, *remaining_parts].join('.')
[auth_data, *remaining_parts].join(".")
end

# Decrypts an encrypted JSON Web Token.
#
# @param token [String or Array] The token, or token parts, to decrypt.
# @return [String] The token payload.
def decrypt(token)
parts, decoded_parts = Sandal::Enc.token_parts(token)
cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
Expand All @@ -52,8 +64,8 @@ def decrypt(token)
cipher.auth_tag = decoded_parts[4]
cipher.auth_data = parts.take(2).join('.')
cipher.update(decoded_parts[3]) + cipher.final
rescue OpenSSL::Cipher::CipherError
raise Sandal::InvalidTokenError, 'Cannot decrypt token.'
rescue OpenSSL::Cipher::CipherError => e
raise Sandal::InvalidTokenError, "Cannot decrypt token: #{e.message}"
end
end

Expand All @@ -65,9 +77,13 @@ class A128GCM < Sandal::Enc::AGCM
# The size of key that is required, in bits.
KEY_SIZE = 128

def initialize(key)
super(KEY_SIZE, key)
# Initialises a new instance.
#
# @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
def initialize(alg)
super(KEY_SIZE, alg)
end

end

# The A256GCM encryption method.
Expand All @@ -76,9 +92,13 @@ class A256GCM < Sandal::Enc::AGCM
# The size of key that is required, in bits.
KEY_SIZE = 256

def initialize(key)
super(KEY_SIZE, key)
# Initialises a new instance.
#
# @param alg [#name, #encrypt_key, #decrypt_key] The algorithm to use to encrypt and/or decrypt the AES key.
def initialize(alg)
super(KEY_SIZE, alg)
end

end

end
Expand Down
3 changes: 1 addition & 2 deletions lib/sandal/enc/alg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ module Alg
end

require 'sandal/enc/alg/direct'
require 'sandal/enc/alg/rsa1_5'
require 'sandal/enc/alg/rsa_oaep'
require 'sandal/enc/alg/rsa'

0 comments on commit e078bfe

Please sign in to comment.