Skip to content

Instantly share code, notes, and snippets.

@ryandotsmith
Last active December 16, 2015 00:39
Show Gist options
  • Save ryandotsmith/5349499 to your computer and use it in GitHub Desktop.
Save ryandotsmith/5349499 to your computer and use it in GitHub Desktop.
fernet-lite
#encoding UTF-8
require 'openssl'
require 'base64'
module FernetLite
MAX_CLOCK_SKEW = 60
VERSION = 0x80
def self.split_key(k)
dec = Base64.urlsafe_decode64(k)
[dec.slice(0, dec.size/2), dec.slice(dec.size/2, dec.size)]
end
def self.encrypt_and_sign(key, str, iv=nil)
sign_bits, crypt_bits = split_key(key)
cipher = OpenSSL::Cipher.new('AES-128-CBC')
cipher.encrypt #required to init cipher
#1 byte. Start by initializing our msg with the fernet version number
msg = [VERSION].pack("C")
#8 bytes. we reverse for big endian
t = [Time.now.to_i].pack("q").reverse
msg += t
#16 bytes. will set iv on cipher. iv is returned as well.
#The only reason we accept a pre-defined iv is for testing.
msg += cipher.iv = (iv || cipher.random_iv)
#add encrypted str to msg.
cipher.key = crypt_bits
msg += (cipher.update(str) + cipher.final)
#sign the msg
hmac = OpenSSL::HMAC.digest('sha256', sign_bits, msg)
Base64.urlsafe_encode64(msg + hmac)
end
def self.verify_and_decrypt(key, encoded_str, ttl=60)
sign_bits, crypt_bits = split_key(key)
str = Base64.urlsafe_decode64(encoded_str)
#The last 32 bytes of msg is the hmac generated by the encryption step.
#Trim the last 32 bytes off the msg and generate an hmac.
sig_pos = str.length - 32
hmac = OpenSSL::HMAC.digest('sha256', sign_bits, str[0..sig_pos-1])
#Use a constant time comparison to protect against timing attacks.
#Copy the bytes into dup so that we can shift them like an array.
check, dup = 0, str[sig_pos..str.length].bytes.to_a
hmac.each_byte {|b| check |= b ^ dup.shift}
if check != 0
return nil
end
version = str[0..1].unpack("C")[0]
if version.to_s != VERSION.to_s
return nil
end
t = Time.at(str[1..8].reverse.unpack("q").join.to_i)
#Handle clock skew.
if (t - Time.now) > MAX_CLOCK_SKEW
return nil
end
#Verify time is within ttl.
if (Time.now - t) >= ttl
return nil
end
iv = str[9..25]
msg = str[25..sig_pos-1]
decipher = OpenSSL::Cipher.new('AES-128-CBC')
decipher.decrypt
decipher.iv = iv
decipher.key = crypt_bits
decipher.update(msg) + decipher.final
end
end
if __FILE__==$0
#Example Usage:
k = "lMTR7g20KX5tB1yq4j/fqvKdu8NTHgr3x1cS3kDY1OA="
puts FernetLite.verify_and_decrypt(k,
FernetLite.encrypt_and_sign(k, "hello world"))
#Test code against fernet spec.
#https://github.com/kr/fernet-spec
require 'open-uri'
require 'json'
genspec = open('https://raw.github.com/kr/fernet-spec/master/generate.json')
JSON.parse(genspec.read).each do |c|
Time.class_eval do
@t = Time.parse(c['now'])
def self.now
@t
end
end
res = FernetLite.encrypt_and_sign(c['secret'], c['src'], c['iv'].pack('C*'))
if res != c['token']
puts "status=fail test=generate actual=#{res} expected=#{c['token']}"
else
puts "status=ok test=generate"
end
end
verspec = open('https://raw.github.com/kr/fernet-spec/master/verify.json')
JSON.parse(verspec.read).each do |c|
Time.class_eval do
@t = Time.parse(c['now'])
def self.now
@t
end
end
res = FernetLite.verify_and_decrypt(c['secret'], c['token'])
if res != c['src']
puts "status=fail test=verify actual=#{res} expected=#{c['src']}"
else
puts "status=ok test=verify"
end
end
invspec = open('https://raw.github.com/kr/fernet-spec/master/invalid.json')
JSON.parse(invspec.read).each do |c|
begin
Time.class_eval do
@t = Time.parse(c['now'])
def self.now
@t
end
end
ttl = Float(c['ttl_sec'])
res = FernetLite.verify_and_decrypt(c['secret'], c['token'], ttl)
if res != nil
puts "status=fail test=#{c['desc']} actual=#{res} expected=nil"
else
puts "status=ok test=#{c['desc']}"
end
rescue => e
puts "status=ok caught-error=#{e.message}"
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment