Last active
December 16, 2015 00:39
-
-
Save ryandotsmith/5349499 to your computer and use it in GitHub Desktop.
fernet-lite
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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