Skip to content

Commit

Permalink
Create the hkdf gem
Browse files Browse the repository at this point in the history
  • Loading branch information
jtdowney committed Apr 14, 2012
0 parents commit 3d8c122
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.gem
.bundle
Gemfile.lock
pkg/*
2 changes: 2 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--order rand
--color
48 changes: 48 additions & 0 deletions .rvmrc
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
source "http://rubygems.org"
gemspec
11 changes: 11 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -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 changes: 15 additions & 0 deletions hkdf.gemspec
Original file line number Diff line number Diff line change
@@ -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 changes: 60 additions & 0 deletions lib/hkdf.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 101 additions & 0 deletions spec/hkdf_spec.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 53 additions & 0 deletions spec/test_vectors.txt
Original file line number Diff line number Diff line change
@@ -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.