lib/opentoken.rb in opentoken-0.2.1 vs lib/opentoken.rb in opentoken-1.0.0

- old
+ new

@@ -2,17 +2,17 @@ require 'openssl' require 'digest/sha1' require 'zlib' require 'stringio' require 'cgi' +require File.join(File.dirname(__FILE__), 'opentoken', 'token') require File.join(File.dirname(__FILE__), 'opentoken', 'key_value_serializer') require File.join(File.dirname(__FILE__), 'opentoken', 'password_key_generator') -class OpenToken - class TokenExpiredError < StandardError; end +module OpenToken + class TokenInvalidError < StandardError; end - DEBUG = false CIPHER_NULL = 0 CIPHER_AES_256_CBC = 1 CIPHER_AES_128_CBC = 2 CIPHER_3DES_168_CBC = 3 @@ -35,125 +35,130 @@ :iv_length => 8, :key_length => 168 } } - def initialize(token, options = {}) - #ruby 1.9 has Base64.urlsafe_decode64 which can be used instead of gsubbing '_' and '-' - string = (token || '').gsub('*', '=').gsub('_', '/').gsub('-', '+') - data = Base64.decode64(string) - inspect_binary_string 'DATA', data + class << self + @@debug = nil + def debug=(flag) + @@debug = flag + end + def debug? + @@debug + end + @@password = nil + def password=(password) + @@password = password + end + def parse(opentoken = nil) + verify opentoken.present?, 'Unable to parse empty token' + data = decode(opentoken) + inspect_binary_string 'DATA', data - #header: should be OTK - header = data[0..2] - raise "Invalid token header: #{header}" unless header == 'OTK' + verify_header data + verify_version data - #version: should == 1 - version = data[3] - raise "Unsupported token version: #{version}" unless version == 1 + #cipher suite identifier + cipher_suite = data[4] + cipher = CIPHERS[cipher_suite] + verify !cipher.nil?, "Unknown cipher suite: #{cipher_suite}" - #cipher suite identifier - cipher_suite = data[4] - cipher = CIPHERS[cipher_suite] - raise "Unknown cipher suite: #{cipher_suite}" if cipher.nil? + #SHA-1 HMAC + payload_hmac = data[5..24] + inspect_binary_string "PAYLOAD HMAC [5..24]", payload_hmac - #SHA-1 HMAC - payload_hmac = data[5..24] - inspect_binary_string "PAYLOAD HMAC [5..24]", payload_hmac + #Initialization Vector (iv) + iv_length = data[25] + iv_end = [26, 26 + iv_length - 1].max + iv = data[26..iv_end] + inspect_binary_string "IV [26..#{iv_end}]", iv + verify iv_length == cipher[:iv_length], "Cipher expects iv length of #{cipher[:iv_length]} and was: #{iv_length}" - #Initialization Vector (iv) - iv_length = data[25] - iv_end = [26, 26 + iv_length - 1].max - iv = data[26..iv_end] - inspect_binary_string "IV [26..#{iv_end}]", iv - raise "Cipher expects iv length of #{cipher[:iv_length]} and was: #{iv_length}" unless iv_length == cipher[:iv_length] + #key (not currently used) + key_length = data[iv_end + 1] + key_end = iv_end + 1 + verify key_length == 0, "Token key embedding is not currently supported" - #key (not currently used) - key_length = data[iv_end + 1] - key_end = iv_end + 1 - raise "Token key embedding is not currently supported" unless key_length == 0 + #payload + payload_length = data[(key_end + 1)..(key_end + 2)].unpack('n').first + payload_offset = key_end + 3 + encrypted_payload = data[payload_offset..(data.length - 1)] + verify encrypted_payload.length == payload_length, "Payload length is #{encrypted_payload.length} and was expected to be #{payload_length}" + inspect_binary_string "ENCRYPTED PAYLOAD [#{payload_offset}..#{data.length - 1}]", encrypted_payload - #payload - payload_length = data[(key_end + 1)..(key_end + 2)].unpack('n').first - payload_offset = key_end + 3 - encrypted_payload = data[payload_offset..(data.length - 1)] - raise "Payload length is #{encrypted_payload.length} and was expected to be #{payload_length}" unless encrypted_payload.length == payload_length - inspect_binary_string "ENCRYPTED PAYLOAD [#{payload_offset}..#{data.length - 1}]", encrypted_payload + key = OpenToken::PasswordKeyGenerator.generate(@@password, cipher) + inspect_binary_string 'KEY', key - key = PasswordKeyGenerator.generate(options[:password], cipher) - inspect_binary_string 'KEY', key + compressed_payload = decrypt_payload(encrypted_payload, cipher, key, iv) + inspect_binary_string 'COMPRESSED PAYLOAD', compressed_payload - compressed_payload = decrypt_payload(encrypted_payload, cipher, key, iv) - inspect_binary_string 'COMPRESSED PAYLOAD', compressed_payload + unparsed_payload = unzip_payload compressed_payload + puts 'EXPANDED PAYLOAD', unparsed_payload if debug? - #decompress the payload - #see http://stackoverflow.com/questions/1361892/how-to-decompress-gzip-data-in-ruby - unparsed_payload = begin - Zlib::Inflate.inflate(compressed_payload) - rescue Zlib::BufError - Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(compressed_payload[2, compressed_payload.size]) - end - puts 'EXPANDED PAYLOAD', unparsed_payload if DEBUG + #validate payload hmac + mac = [] + mac << "0x01".hex.chr + mac << cipher_suite.chr + mac << iv + mac << key if key_length > 0 #key embedding is not currently supported + mac << unparsed_payload + hash = OpenSSL::HMAC.digest(OpenToken::PasswordKeyGenerator::SHA1_DIGEST, key, mac.join) + if (hash <=> payload_hmac) != 0 + verify payload_hmac == hash, "HMAC for payload was #{hash} and expected to be #{payload_hmac}" + end - #validate payload hmac - mac = "0x01".hex.chr - mac += cipher_suite.chr - mac += iv - mac += key if key_length > 0 #key embedding is not currently supported - mac += unparsed_payload - hash = OpenSSL::HMAC.digest(PasswordKeyGenerator::SHA1_DIGEST, key, mac) - if (hash <=> payload_hmac) != 0 - raise "HMAC for payload was #{hash} and expected to be #{payload_hmac}" unless payload_hmac == hash + unescaped_payload = CGI::unescapeHTML(unparsed_payload) + puts 'UNESCAPED PAYLOAD', unescaped_payload if debug? + token = OpenToken::KeyValueSerializer.deserialize unescaped_payload + puts token.inspect if debug? + token.validate! + token end - unescaped_payload = CGI::unescapeHTML(unparsed_payload) - puts 'UNESCAPED PAYLOAD', unescaped_payload if DEBUG - @payload = KeyValueSerializer.deserialize unescaped_payload - puts @payload.inspect if DEBUG - raise TokenExpiredError.new("#{Time.now.utc} is not within token duration: #{self.start_at} - #{self.end_at}") if self.expired? - end - - def [](key) - @payload[key.to_s] - end - #verify that the current time is between the not-before and not-on-or-after values - def expired? - now = Time.now.utc - now < start_at || now >= end_at - end - def start_at - payload_date('not-before') - end - def end_at - payload_date('not-on-or-after') - end - #"renew-until"=>"2010-03-05T07:19:15Z" - def valid_until - payload_date('renew-until') - end - def payload_date(key) - Time.iso8601(self[key]).utc - end - - private - def decrypt_payload(encrypted_payload, cipher, key, iv) - return encrypted_payload unless cipher[:algorithm] + private + def verify_header(data) + header = data[0..2] + verify header == 'OTK', "Invalid token header: #{header}" + end + def verify_version(data) + version = data[3] + verify version == 1, "Unsupported token version: #{version}" + end + #ruby 1.9 has Base64.urlsafe_decode64 which can be used instead of gsubbing '_' and '-' + def decode(token) + string = token.gsub('*', '=').gsub('_', '/').gsub('-', '+') + data = Base64.decode64(string) + end + def verify(assertion, message = 'Invalid Token') + raise OpenToken::TokenInvalidError.new(message) unless assertion + end #see http://snippets.dzone.com/posts/show/4975 #see http://jdwyah.blogspot.com/2009/12/decrypting-ruby-aes-encryption.html #see http://snippets.dzone.com/posts/show/576 - crypt = OpenSSL::Cipher::Cipher.new(cipher[:algorithm]) - crypt.decrypt - crypt.key = key - crypt.iv = iv - crypt.update(encrypted_payload) + crypt.final - end - - def inspect_binary_string(header, string) - return unless DEBUG - puts "#{header}:" - index = 0 - string.each_byte do |b| - puts "#{index}: #{b} => #{b.chr}" - index += 1 + def decrypt_payload(encrypted_payload, cipher, key, iv) + return encrypted_payload unless cipher[:algorithm] + crypt = OpenSSL::Cipher::Cipher.new(cipher[:algorithm]) + crypt.decrypt + crypt.key = key + crypt.iv = iv + crypt.update(encrypted_payload) + crypt.final + end + #decompress the payload + #see http://stackoverflow.com/questions/1361892/how-to-decompress-gzip-data-in-ruby + def unzip_payload(compressed_payload) + unparsed_payload = begin + Zlib::Inflate.inflate(compressed_payload) + rescue Zlib::BufError + Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(compressed_payload[2, compressed_payload.size]) + end + end + def inspect_binary_string(header, string) + return unless debug? + puts "#{header}:" + index = 0 + string.each_byte do |b| + puts "#{index}: #{b} => #{b.chr}" + index += 1 + end end end end