Skip to content
Browse files

Create the hkdf gem

  • Loading branch information...
0 parents commit 3d8c122b53e8195fec70d431d5cf1774ad5874de @jtdowney committed Apr 14, 2012
Showing with 315 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +2 −0 .rspec
  3. +48 −0 .rvmrc
  4. +2 −0 Gemfile
  5. +11 −0 Rakefile
  6. +15 −0 hkdf.gemspec
  7. +60 −0 lib/hkdf.rb
  8. +101 −0 spec/hkdf_spec.rb
  9. +19 −0 spec/spec_helper.rb
  10. +53 −0 spec/test_vectors.txt
4 .gitignore
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
2 .rspec
@@ -0,0 +1,2 @@
+--order rand
+--color
48 .rvmrc
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+# This is an RVM Project .rvmrc file, used to automatically load the ruby
+# development environment upon cd'ing into the directory
+
+# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
+# Only full ruby name is supported here, for short names use:
+# echo "rvm use 1.9.3" > .rvmrc
+environment_id="ruby-1.9.3@hkdf"
+
+# Uncomment the following lines if you want to verify rvm version per project
+# rvmrc_rvm_version="1.12.2 (stable)" # 1.10.1 seams as a safe start
+# eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
+# echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
+# return 1
+# }
+
+# First we attempt to load the desired environment directly from the environment
+# file. This is very fast and efficient compared to running through the entire
+# CLI and selector. If you want feedback on which environment was used then
+# insert the word 'use' after --create as this triggers verbose mode.
+if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
+then
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
+ [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
+ \. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
+else
+ # If the environment file has not yet been created, use the RVM CLI to select.
+ rvm --create "$environment_id" || {
+ echo "Failed to create RVM environment '${environment_id}'."
+ return 1
+ }
+fi
+
+# If you use bundler, this might be useful to you:
+# if [[ -s Gemfile ]] && {
+# ! builtin command -v bundle >/dev/null ||
+# builtin command -v bundle | grep $rvm_path/bin/bundle >/dev/null
+# }
+# then
+# printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
+# gem install bundler
+# fi
+# if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
+# then
+# bundle install | grep -vE '^Using|Your bundle is complete'
+# fi
2 Gemfile
@@ -0,0 +1,2 @@
+source "http://rubygems.org"
+gemspec
11 Rakefile
@@ -0,0 +1,11 @@
+require 'bundler/gem_tasks'
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new
+
+desc 'Open an irb session preloaded with the library'
+task :console do
+ sh "irb -rubygems -r hkdf -I lib"
+end
+
+task :default => :spec
15 hkdf.gemspec
@@ -0,0 +1,15 @@
+Gem::Specification.new do |s|
+ s.name = 'hkdf'
+ s.version = '0.1.0'
+ s.authors = ['John Downey']
+ s.email = ['jdowney@gmail.com']
+ s.homepage = 'http://github.com/jtdowney/hkdf'
+ s.summary = %q{HMAC-based Key Derivation Function}
+ s.description = %q{A ruby implementation of RFC5869: HMAC-based Extract-and-Expand Key Derivation Function (HKDF). The goal of HKDF is to take some source key material and generate suitable cryptographic keys from it.}
+
+ s.files = Dir.glob('lib/**/*')
+ s.test_files = Dir.glob('spec/**/*')
+ s.require_paths = ['lib']
+
+ s.add_development_dependency 'rspec'
+end
60 lib/hkdf.rb
@@ -0,0 +1,60 @@
+require 'openssl'
+
+class HKDF
+ def initialize(source, options = {})
+ options = {:algorithm => 'SHA256', :info => '', :salt => nil}.merge(options)
+
+ @digest = OpenSSL::Digest.new(options[:algorithm])
+ @info = options[:info]
+
+ salt = options[:salt]
+ salt = 0.chr * @digest.digest_length if salt.nil? #or salt.empty?
+
+ @prk = OpenSSL::HMAC.digest(@digest, salt, source)
+ @position = 0
+ @blocks = []
+ @blocks << ''
+ end
+
+ def algorithm
+ @digest.name
+ end
+
+ def max_length
+ @digest.digest_length * 255
+ end
+
+ def seek(position)
+ raise RangeError.new("cannot seek past #{max_length}") if position > max_length
+
+ @position = position
+ end
+
+ def rewind
+ seek(0)
+ end
+
+ def next_bytes(length)
+ new_position = length + @position
+ raise RangeError.new("requested #{length} bytes, only #{max_length} available") if new_position > max_length
+
+ _generate_blocks(new_position)
+
+ start = @position
+ @position = new_position
+
+ @blocks.join('').slice(start, length)
+ end
+
+ def next_hex_bytes(length)
+ next_bytes(length).unpack('H*').first
+ end
+
+ def _generate_blocks(length)
+ start = @blocks.size
+ block_count = (length.to_f / @digest.digest_length).ceil
+ start.upto(block_count) do |n|
+ @blocks << OpenSSL::HMAC.digest(@digest, @prk, @blocks[n - 1] + @info + n.chr)
+ end
+ end
+end
101 spec/hkdf_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe HKDF do
+ before(:each) do
+ @algorithm = 'SHA256'
+ @source = 'source'
+ @hkdf = HKDF.new(@source, :algorithm => @algorithm)
+ end
+
+ describe 'initialize' do
+ it 'defaults the algorithm to SHA-256' do
+ HKDF.new(@source).algorithm.should == 'SHA256'
+ end
+
+ it 'takes an optional digest algorithm' do
+ @hkdf = HKDF.new('source', :algorithm => 'SHA1')
+ @hkdf.algorithm.should == 'SHA1'
+ end
+
+ it 'defaults salt to all zeros of digest length' do
+ salt = 0.chr * 32
+
+ @hkdf_salt = HKDF.new(@source, :algorithm => @algorithm, :salt => salt)
+ @hkdf_nosalt = HKDF.new(@source, :algorithm => @algorithm)
+ @hkdf_salt.next_bytes(32) == @hkdf_nosalt.next_bytes(32)
+ end
+
+ it 'sets salt to all zeros of digest 32 if empty' do
+ @hkdf_blanksalt = HKDF.new(@source, :algorithm => @algorithm, :salt => '')
+ @hkdf_nosalt = HKDF.new(@source, :algorithm => @algorithm)
+ @hkdf_blanksalt.next_bytes(32) == @hkdf_nosalt.next_bytes(32)
+ end
+
+ it 'defaults info to an empty string' do
+ @hkdf_info = HKDF.new(@source, :algorithm => @algorithm, :info => '')
+ @hkdf_noinfo = HKDF.new(@source, :algorithm => @algorithm)
+ @hkdf_info.next_bytes(32) == @hkdf_noinfo.next_bytes(32)
+ end
+ end
+
+ describe 'max_length' do
+ it 'is 255 times the digest length' do
+ @hkdf.max_length.should == 255 * 32
+ end
+ end
+
+ describe 'next_bytes' do
+ it 'raises an error if requested size is > max_length' do
+ expect { @hkdf.next_bytes(@hkdf.max_length + 1) }.should raise_error(RangeError, /requested \d+ bytes, only \d+ available/)
+ expect { @hkdf.next_bytes(@hkdf.max_length) }.should_not raise_error(RangeError, /requested \d+ bytes, only \d+ available/)
+ end
+
+ it 'raises an error if requested size + current position is > max_length' do
+ expect do
+ @hkdf.next_bytes(32)
+ @hkdf.next_bytes(@hkdf.max_length - 31)
+ end.should raise_error(RangeError, /requested \d+ bytes, only \d+ available/)
+ end
+
+ it 'advances the stream position' do
+ @hkdf.next_bytes(32).should_not == @hkdf.next_bytes(32)
+ end
+
+ test_vectors.each do |name, options|
+ it "matches output from the '#{name}' test vector" do
+ options[:algorithm] = options[:Hash]
+
+ hkdf = HKDF.new(options[:IKM], options)
+ hkdf.next_bytes(options[:L].to_i).should == options[:OKM]
+ end
+ end
+ end
+
+ describe 'next_hex_bytes' do
+ it 'returns the next bytes as hex' do
+ @hkdf.next_hex_bytes(20).should == 'fb496612b8cb82cd2297770f83c72b377af16d7b'
+ end
+ end
+
+ describe 'seek' do
+ it 'sets the position anywhere in the stream' do
+ @hkdf.next_bytes(10)
+ output = @hkdf.next_bytes(32)
+ @hkdf.seek(10)
+ @hkdf.next_bytes(32).should == output
+ end
+
+ it 'raises an error if requested to seek past end of stream' do
+ expect { @hkdf.seek(@hkdf.max_length + 1) }.should raise_error(RangeError, /cannot seek past \d+/)
+ expect { @hkdf.seek(@hkdf.max_length) }.should_not raise_error(RangeError, /cannot seek past \d+/)
+ end
+ end
+
+ describe 'rewind' do
+ it 'resets the stream position to the beginning' do
+ output = @hkdf.next_bytes(32)
+ @hkdf.rewind
+ @hkdf.next_bytes(32).should == output
+ end
+ end
+end
19 spec/spec_helper.rb
@@ -0,0 +1,19 @@
+require 'hkdf'
+
+def test_vectors
+ test_lines = File.readlines('spec/test_vectors.txt').map(&:strip).reject(&:empty?)
+
+ vectors = {}
+ test_lines.each_slice(8) do |lines|
+ name = lines.shift
+ values = lines.inject({}) do |hash, line|
+ key, value = line.split('=').map(&:strip)
+ value = '' unless value
+ value = [value.slice(2..-1)].pack('H*') if value.start_with?('0x')
+ hash[key.to_sym] = value
+ hash
+ end
+ vectors[name] = values
+ end
+ vectors
+end
53 spec/test_vectors.txt
@@ -0,0 +1,53 @@
+Basic test case with SHA256
+Hash = SHA256
+IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
+salt = 0x000102030405060708090a0b0c
+info = 0xf0f1f2f3f4f5f6f7f8f9
+L = 42
+PRK = 0x077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5
+OKM = 0x3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
+
+Test with SHA256 and longer inputs/outputs
+Hash = SHA256
+IKM = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f
+salt = 0x606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf
+info = 0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff
+L = 82
+PRK = 0x06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244
+OKM = 0xb11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87
+
+Test with SHA256 and empty salt/info
+Hash = SHA256
+IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
+salt =
+info =
+L = 42
+PRK = 0x19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04
+OKM = 0x8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8
+
+Basic test case with SHA1
+Hash = SHA1
+IKM = 0x0b0b0b0b0b0b0b0b0b0b0b
+salt = 0x000102030405060708090a0b0c
+info = 0xf0f1f2f3f4f5f6f7f8f9
+L = 42
+PRK = 0x9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243
+OKM = 0x085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896
+
+Test with SHA1 and longer inputs/outputs
+Hash = SHA1
+IKM = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f
+salt = 0x606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf
+info = 0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff
+L = 82
+PRK = 0x8adae09a2a307059478d309b26c4115a224cfaf6
+OKM = 0x0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4
+
+Test with SHA1 and empty salt/info
+Hash = SHA1
+IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
+salt =
+info =
+L = 42
+PRK = 0xda8c8a73c7fa77288ec6f5e7c297786aa0d32d01
+OKM = 0x0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918

0 comments on commit 3d8c122

Please sign in to comment.
Something went wrong with that request. Please try again.