Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AES192 and AES256 as optional encryption methods #38

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ test/version_tmp
tmp
.rbenv-version
.ruby-version
.ruby-gemset
3 changes: 2 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[submodule "spec/fernet-spec"]
path = spec/fernet-spec
url = git://github.com/kr/fernet-spec.git
url = https://github.com/tknarr/fernet-spec.git
branch = enhance_aes
6 changes: 3 additions & 3 deletions fernet.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
require File.expand_path('../lib/fernet/version', __FILE__)

Gem::Specification.new do |gem|
gem.authors = ["Harold Giménez"]
gem.email = ["harold.gimenez@gmail.com"]
gem.description = "Delicious HMAC Digest(if) authentication and AES-128-CBC encryption"
gem.authors = ["Harold Giménez", "Todd Knarr"]
gem.email = ["harold.gimenez@gmail.com", "tknarr@silverglass.org"]
gem.description = "Delicious HMAC Digest(if) authentication and AES-128/192/256-CBC encryption"
gem.summary = "Easily generate and verify AES encrypted HMAC based authentication tokens"
gem.homepage = "https://github.com/fernet/fernet-rb"

Expand Down
10 changes: 7 additions & 3 deletions lib/fernet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
Fernet::Configuration.run

module Fernet
# Determine AES key bits from base64-encoded secret length
KEYBITS_SELECT = { 44 => 128, 64 => 192, 88 => 256 }.freeze

# Public: generates a fernet token
#
# secret - a base64 encoded, 32 byte string
# message - the message being secured in plain text
# secret - a base64 encoded 32, 48 or 64 byte string
# message - the message being secured in plain text
#
# Examples
#
Expand All @@ -30,7 +33,8 @@ def self.generate(secret, message = '', opts = {})
# better than just returning ASCII with mangled unicode bytes in it.
message = message.encode(Encoding::UTF_8) if message

Generator.new(opts.merge({secret: secret, message: message})).
key_bits = KEYBITS_SELECT[secret.bytesize] || 128
Generator.new(opts.merge({secret: secret, message: message, key_bits: key_bits})).
generate
end

Expand Down
8 changes: 6 additions & 2 deletions lib/fernet/encryption.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ module Encryption
#
# Returns a two-element array containing the ciphertext and the random IV
def self.encrypt(opts)
cipher = OpenSSL::Cipher.new('AES-128-CBC')
key_bits = opts[:key].bytesize * 8
cipher_selection = 'AES-' + key_bits.to_s + '-CBC'
cipher = OpenSSL::Cipher.new(cipher_selection)
cipher.encrypt
iv = opts[:iv] || cipher.random_iv
cipher.iv = iv
Expand Down Expand Up @@ -50,7 +52,9 @@ def self.encrypt(opts)
#
# Returns a two-element array containing the ciphertext and the random IV
def self.decrypt(opts)
decipher = OpenSSL::Cipher.new('AES-128-CBC')
key_bits = opts[:key].bytesize * 8
cipher_selection = 'AES-' + key_bits.to_s + '-CBC'
decipher = OpenSSL::Cipher.new(cipher_selection)
decipher.decrypt
decipher.iv = opts[:iv]
decipher.key = opts[:key]
Expand Down
25 changes: 14 additions & 11 deletions lib/fernet/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ class Generator
# Internal: Initializes a generator
#
# opts - a hash containing the following keys:
# * secret - a string containing a secret, optionally Base64 encoded
# * message - the message
# * key_bits - number of bits in the AES key, defaults to 128
# * secret - a string containing a secret, optionally Base64 encoded
# * message - the message
def initialize(opts)
@secret = opts.fetch(:secret)
@message = opts[:message]
@iv = opts[:iv]
@now = opts[:now]
@key_bits = opts[:key_bits] || 128
@secret = opts.fetch(:secret)
@message = opts[:message]
@iv = opts[:iv]
@now = opts[:now]
end

# Internal: generates a secret token
Expand All @@ -40,17 +42,18 @@ def initialize(opts)
def generate
yield self if block_given?

token = Token.generate(secret: @secret,
message: @message,
iv: @iv,
now: @now)
token = Token.generate(key_bits: @key_bits,
secret: @secret,
message: @message,
iv: @iv,
now: @now)
token.to_s
end

# Public: string representation of this generator, masks secret to avoid
# leaks
def inspect
"#<Fernet::Generator @secret=[masked] @message=#{@message.inspect}>"
"#<Fernet::Generator @key_bits=#{@key_bits} @secret=[masked] @message=#{@message.inspect}>"
end
alias to_s inspect

Expand Down
28 changes: 19 additions & 9 deletions lib/fernet/secret.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,50 @@ class InvalidSecret < Fernet::Error; end

# Internal - Initialize a Secret
#
# secret - the secret, optionally encoded with either standard or
# URL safe variants of Base64 encoding
# secret - the secret, optionally encoded with either standard or
# URL safe variants of Base64 encoding
# key_bits - number of bits in the AES key
#
# Raises Fernet::Secret::InvalidSecret if it cannot be decoded or is
# not of the expected length
def initialize(secret)
if secret.bytesize == 32
def initialize(secret, key_bits = 128)
@key_bytes = key_bits / 8
if secret.bytesize == @key_bytes * 2
@secret = secret
else
begin
@secret = Base64.urlsafe_decode64(secret)
rescue ArgumentError
@secret = Base64.decode64(secret)
end
unless @secret.bytesize == 32
unless @secret.bytesize == @key_bytes * 2
raise InvalidSecret,
"Secret must be 32 bytes, instead got #{@secret.bytesize}"
"Secret must be #{@key_bytes * 2} bytes, instead got #{@secret.bytesize}"
end
end
end

# Internal: Returns the portion of the secret token used for encryption
def encryption_key
@secret.slice(16, 16)
@secret.slice(@key_bytes, @key_bytes)
end

# Internal: Returns the portion of the secret token used for signing
def signing_key
@secret.slice(0, 16)
@secret.slice(0, @key_bytes)
end

# Public: AES key size in bytes and bits
def key_bytes
@key_bytes
end
def key_bits
@key_bytes * 8
end

# Public: String representation of this secret, masks to avoid leaks.
def to_s
"<Fernet::Secret [masked]>"
"<Fernet::Secret key_bits=#{@key_bytes * 8} [masked]>"
end
alias to_s inspect
end
Expand Down
46 changes: 40 additions & 6 deletions lib/fernet/token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class InvalidToken < Fernet::Error; end

# Internal: the default token version
DEFAULT_VERSION = 0x80.freeze
# Internet: mapping from key size to version byte
VALID_VERSIONS = { 128 => 0x80, 192 => 0xA0, 256 => 0xC0 }.freeze
# Internal: max allowed clock skew for calculating TTL
MAX_CLOCK_SKEW = 60.freeze

Expand All @@ -26,7 +28,13 @@ class InvalidToken < Fernet::Error; end
# Configuration.ttl
def initialize(token, opts = {})
@token = token
@secret = Secret.new(opts.fetch(:secret))
begin
version_byte = version
rescue
version_byte = DEFAULT_VERSION
end
key_bits, _ = VALID_VERSIONS.rassoc(version_byte) || [ 128, 0 ]
@secret = Secret.new(opts.fetch(:secret), key_bits)
@enforce_ttl = opts.fetch(:enforce_ttl) { Configuration.enforce_ttl }
@ttl = opts[:ttl] || Configuration.ttl
@now = opts[:now]
Expand Down Expand Up @@ -67,21 +75,23 @@ def message
# Internal: generates a Fernet Token
#
# opts - a hash containing
# * secret - a string containing the secret, optionally base64 encoded
# * message - the message in plain text
# * secret - a string containing the secret, optionally base64 encoded
# * message - the message in plain text
# * key_bits - number of bits in the AES key
def self.generate(opts)
unless opts[:secret]
raise ArgumentError, 'Secret not provided'
end
secret = Secret.new(opts.fetch(:secret))
key_bits = opts[:key_bits] || 128
secret = Secret.new(opts.fetch(:secret), key_bits)
encrypted_message, iv = Encryption.encrypt(
key: secret.encryption_key,
message: opts[:message],
iv: opts[:iv]
)
issued_timestamp = (opts[:now] || Time.now).to_i

version = opts[:version] || DEFAULT_VERSION
version = opts[:version] || VALID_VERSIONS[key_bits] || DEFAULT_VERSION
payload = [version].pack("C") +
BitPacking.pack_int64_bigendian(issued_timestamp) +
iv +
Expand All @@ -90,6 +100,30 @@ def self.generate(opts)
new(Base64.urlsafe_encode64(payload + mac), secret: opts.fetch(:secret))
end

# Internal: return the AES key length in bits based on a version byte
#
# v - version byte
def self.version_key_bits(v)
(VALID_VERSIONS.rassoc(v) || [ 0, 0 ])[0]
end

# Internal: return the revision of the spec based on a version byte
#
# v - version byte
def self.version_revision(v)
v & 0b00011111
end

# Internal: build a version byte based on a revision and AES key length
#
# key_bits - number of bits in the AES key
# revision - revision of the spec
def self.make_version(key_bits, revision)
base_version = VALID_VERSIONS[key_bits]
return 0 if base_version.nil?
( base_version & 0b11100000 ) | ( revision & 0b00011111 )
end

private
def decoded_token
@decoded_token ||= Base64.urlsafe_decode64(@token)
Expand Down Expand Up @@ -176,7 +210,7 @@ def ciphertext_multiple_of_block_size?
end

def unknown_token_version?
DEFAULT_VERSION != version
VALID_VERSIONS.rassoc(version).nil?
end

def enforce_ttl?
Expand Down
2 changes: 1 addition & 1 deletion lib/fernet/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Fernet
VERSION = "2.2"
VERSION = "3.0"
end
55 changes: 50 additions & 5 deletions spec/acceptance/generate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require 'base64'

describe Fernet::Generator do
it 'generates tokens according to the spec' do
it 'generates tokens according to the spec for AES128' do
path = File.expand_path(
'./../fernet-spec/generate.json', File.dirname(__FILE__)
)
Expand All @@ -16,12 +16,57 @@
now = DateTime.parse(test_data['now']).to_time
expected_token = test_data['token']

generator = Fernet::Generator.new(secret: secret,
message: message,
iv: iv,
now: now)
generator = Fernet::Generator.new(secret: secret,
message: message,
iv: iv,
now: now)

expect(generator.generate).to eq(expected_token)
end
end

it 'generates tokens according to the spec for AES192' do
path = File.expand_path(
'./../fernet-spec/generate_192.json', File.dirname(__FILE__)
)
generate_json = JSON.parse(File.read(path))
generate_json.each do |test_data|
message = test_data['src']
iv = test_data['iv'].pack("C*")
secret = test_data['secret']
now = DateTime.parse(test_data['now']).to_time
expected_token = test_data['token']

generator = Fernet::Generator.new(secret: secret,
key_bits: 192,
message: message,
iv: iv,
now: now)

expect(generator.generate).to eq(expected_token)
end
end

it 'generates tokens according to the spec for AES256' do
path = File.expand_path(
'./../fernet-spec/generate_256.json', File.dirname(__FILE__)
)
generate_json = JSON.parse(File.read(path))
generate_json.each do |test_data|
message = test_data['src']
iv = test_data['iv'].pack("C*")
secret = test_data['secret']
now = DateTime.parse(test_data['now']).to_time
expected_token = test_data['token']

generator = Fernet::Generator.new(secret: secret,
key_bits: 256,
message: message,
iv: iv,
now: now)

expect(generator.generate).to eq(expected_token)
end
end

end
Loading