hkdf 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +23 -0
- data/lib/hkdf.rb +60 -0
- data/spec/hkdf_spec.rb +101 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/test_vectors.txt +53 -0
- metadata +66 -0
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# HKDF [![Build Status](https://secure.travis-ci.org/jtdowney/hkdf.png?branch=master)](http://travis-ci.org/jtdowney/hkdf)
|
2
|
+
|
3
|
+
This is a ruby implementation of [RFC 5869](http://tools.ietf.org/html/rfc5869): HMAC-based Extract-and-Expand Key Derivation Function. The goal of HKDF is to take some source key material and generate suitable cryptographic keys from it.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
hkdf = HKDF.new('source key material')
|
9
|
+
hkdf.next_bytes(32)
|
10
|
+
=> "\f#\xF4b\x98\x9B\x7Fw>|/|k\xF4k\xB7\xB9\x11e\xC5\x92\xD1\fH\xFDG\x94vt\xB4\x14\xCE"
|
11
|
+
```
|
12
|
+
|
13
|
+
The default algorithm is HMAC-SHA256, you can override this and other defaults by providing an options hash during construction.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
hkdf = HKDF.new('source key material', :salt => 'NaCl', :algorithm => 'SHA1', :info => 'the 411')
|
17
|
+
hkdf.next_bytes(16)
|
18
|
+
=> "\xC0<\x13\x85\x8C\x84z\xCE\xC7\xCE+\xFF\x1C\xEB\xE6\xBC"
|
19
|
+
```
|
20
|
+
|
21
|
+
## License
|
22
|
+
|
23
|
+
The HKDF gem is released under the [MIT license](http://www.opensource.org/licenses/MIT).
|
data/lib/hkdf.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class HKDF
|
4
|
+
def initialize(source, options = {})
|
5
|
+
options = {:algorithm => 'SHA256', :info => '', :salt => nil}.merge(options)
|
6
|
+
|
7
|
+
@digest = OpenSSL::Digest.new(options[:algorithm])
|
8
|
+
@info = options[:info]
|
9
|
+
|
10
|
+
salt = options[:salt]
|
11
|
+
salt = 0.chr * @digest.digest_length if salt.nil? or salt.empty?
|
12
|
+
|
13
|
+
@prk = OpenSSL::HMAC.digest(@digest, salt, source)
|
14
|
+
@position = 0
|
15
|
+
@blocks = []
|
16
|
+
@blocks << ''
|
17
|
+
end
|
18
|
+
|
19
|
+
def algorithm
|
20
|
+
@digest.name
|
21
|
+
end
|
22
|
+
|
23
|
+
def max_length
|
24
|
+
@digest.digest_length * 255
|
25
|
+
end
|
26
|
+
|
27
|
+
def seek(position)
|
28
|
+
raise RangeError.new("cannot seek past #{max_length}") if position > max_length
|
29
|
+
|
30
|
+
@position = position
|
31
|
+
end
|
32
|
+
|
33
|
+
def rewind
|
34
|
+
seek(0)
|
35
|
+
end
|
36
|
+
|
37
|
+
def next_bytes(length)
|
38
|
+
new_position = length + @position
|
39
|
+
raise RangeError.new("requested #{length} bytes, only #{max_length} available") if new_position > max_length
|
40
|
+
|
41
|
+
_generate_blocks(new_position)
|
42
|
+
|
43
|
+
start = @position
|
44
|
+
@position = new_position
|
45
|
+
|
46
|
+
@blocks.join('').slice(start, length)
|
47
|
+
end
|
48
|
+
|
49
|
+
def next_hex_bytes(length)
|
50
|
+
next_bytes(length).unpack('H*').first
|
51
|
+
end
|
52
|
+
|
53
|
+
def _generate_blocks(length)
|
54
|
+
start = @blocks.size
|
55
|
+
block_count = (length.to_f / @digest.digest_length).ceil
|
56
|
+
start.upto(block_count) do |n|
|
57
|
+
@blocks << OpenSSL::HMAC.digest(@digest, @prk, @blocks[n - 1] + @info + n.chr)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/spec/hkdf_spec.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HKDF do
|
4
|
+
before(:each) do
|
5
|
+
@algorithm = 'SHA256'
|
6
|
+
@source = 'source'
|
7
|
+
@hkdf = HKDF.new(@source, :algorithm => @algorithm)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'initialize' do
|
11
|
+
it 'defaults the algorithm to SHA-256' do
|
12
|
+
HKDF.new(@source).algorithm.should == 'SHA256'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'takes an optional digest algorithm' do
|
16
|
+
@hkdf = HKDF.new('source', :algorithm => 'SHA1')
|
17
|
+
@hkdf.algorithm.should == 'SHA1'
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'defaults salt to all zeros of digest length' do
|
21
|
+
salt = 0.chr * 32
|
22
|
+
|
23
|
+
@hkdf_salt = HKDF.new(@source, :algorithm => @algorithm, :salt => salt)
|
24
|
+
@hkdf_nosalt = HKDF.new(@source, :algorithm => @algorithm)
|
25
|
+
@hkdf_salt.next_bytes(32) == @hkdf_nosalt.next_bytes(32)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'sets salt to all zeros of digest 32 if empty' do
|
29
|
+
@hkdf_blanksalt = HKDF.new(@source, :algorithm => @algorithm, :salt => '')
|
30
|
+
@hkdf_nosalt = HKDF.new(@source, :algorithm => @algorithm)
|
31
|
+
@hkdf_blanksalt.next_bytes(32) == @hkdf_nosalt.next_bytes(32)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'defaults info to an empty string' do
|
35
|
+
@hkdf_info = HKDF.new(@source, :algorithm => @algorithm, :info => '')
|
36
|
+
@hkdf_noinfo = HKDF.new(@source, :algorithm => @algorithm)
|
37
|
+
@hkdf_info.next_bytes(32) == @hkdf_noinfo.next_bytes(32)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'max_length' do
|
42
|
+
it 'is 255 times the digest length' do
|
43
|
+
@hkdf.max_length.should == 255 * 32
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'next_bytes' do
|
48
|
+
it 'raises an error if requested size is > max_length' do
|
49
|
+
expect { @hkdf.next_bytes(@hkdf.max_length + 1) }.should raise_error(RangeError, /requested \d+ bytes, only \d+ available/)
|
50
|
+
expect { @hkdf.next_bytes(@hkdf.max_length) }.should_not raise_error(RangeError, /requested \d+ bytes, only \d+ available/)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'raises an error if requested size + current position is > max_length' do
|
54
|
+
expect do
|
55
|
+
@hkdf.next_bytes(32)
|
56
|
+
@hkdf.next_bytes(@hkdf.max_length - 31)
|
57
|
+
end.should raise_error(RangeError, /requested \d+ bytes, only \d+ available/)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'advances the stream position' do
|
61
|
+
@hkdf.next_bytes(32).should_not == @hkdf.next_bytes(32)
|
62
|
+
end
|
63
|
+
|
64
|
+
test_vectors.each do |name, options|
|
65
|
+
it "matches output from the '#{name}' test vector" do
|
66
|
+
options[:algorithm] = options[:Hash]
|
67
|
+
|
68
|
+
hkdf = HKDF.new(options[:IKM], options)
|
69
|
+
hkdf.next_bytes(options[:L].to_i).should == options[:OKM]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'next_hex_bytes' do
|
75
|
+
it 'returns the next bytes as hex' do
|
76
|
+
@hkdf.next_hex_bytes(20).should == 'fb496612b8cb82cd2297770f83c72b377af16d7b'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'seek' do
|
81
|
+
it 'sets the position anywhere in the stream' do
|
82
|
+
@hkdf.next_bytes(10)
|
83
|
+
output = @hkdf.next_bytes(32)
|
84
|
+
@hkdf.seek(10)
|
85
|
+
@hkdf.next_bytes(32).should == output
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'raises an error if requested to seek past end of stream' do
|
89
|
+
expect { @hkdf.seek(@hkdf.max_length + 1) }.should raise_error(RangeError, /cannot seek past \d+/)
|
90
|
+
expect { @hkdf.seek(@hkdf.max_length) }.should_not raise_error(RangeError, /cannot seek past \d+/)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe 'rewind' do
|
95
|
+
it 'resets the stream position to the beginning' do
|
96
|
+
output = @hkdf.next_bytes(32)
|
97
|
+
@hkdf.rewind
|
98
|
+
@hkdf.next_bytes(32).should == output
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'hkdf'
|
2
|
+
|
3
|
+
def test_vectors
|
4
|
+
test_lines = File.readlines('spec/test_vectors.txt').map(&:strip).reject(&:empty?)
|
5
|
+
|
6
|
+
vectors = {}
|
7
|
+
test_lines.each_slice(8) do |lines|
|
8
|
+
name = lines.shift
|
9
|
+
values = lines.inject({}) do |hash, line|
|
10
|
+
key, value = line.split('=').map(&:strip)
|
11
|
+
value = '' unless value
|
12
|
+
value = [value.slice(2..-1)].pack('H*') if value.start_with?('0x')
|
13
|
+
hash[key.to_sym] = value
|
14
|
+
hash
|
15
|
+
end
|
16
|
+
vectors[name] = values
|
17
|
+
end
|
18
|
+
vectors
|
19
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
Basic test case with SHA256
|
2
|
+
Hash = SHA256
|
3
|
+
IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
|
4
|
+
salt = 0x000102030405060708090a0b0c
|
5
|
+
info = 0xf0f1f2f3f4f5f6f7f8f9
|
6
|
+
L = 42
|
7
|
+
PRK = 0x077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5
|
8
|
+
OKM = 0x3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
|
9
|
+
|
10
|
+
Test with SHA256 and longer inputs/outputs
|
11
|
+
Hash = SHA256
|
12
|
+
IKM = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f
|
13
|
+
salt = 0x606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf
|
14
|
+
info = 0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff
|
15
|
+
L = 82
|
16
|
+
PRK = 0x06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244
|
17
|
+
OKM = 0xb11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87
|
18
|
+
|
19
|
+
Test with SHA256 and empty salt/info
|
20
|
+
Hash = SHA256
|
21
|
+
IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
|
22
|
+
salt =
|
23
|
+
info =
|
24
|
+
L = 42
|
25
|
+
PRK = 0x19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04
|
26
|
+
OKM = 0x8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8
|
27
|
+
|
28
|
+
Basic test case with SHA1
|
29
|
+
Hash = SHA1
|
30
|
+
IKM = 0x0b0b0b0b0b0b0b0b0b0b0b
|
31
|
+
salt = 0x000102030405060708090a0b0c
|
32
|
+
info = 0xf0f1f2f3f4f5f6f7f8f9
|
33
|
+
L = 42
|
34
|
+
PRK = 0x9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243
|
35
|
+
OKM = 0x085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896
|
36
|
+
|
37
|
+
Test with SHA1 and longer inputs/outputs
|
38
|
+
Hash = SHA1
|
39
|
+
IKM = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f
|
40
|
+
salt = 0x606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf
|
41
|
+
info = 0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff
|
42
|
+
L = 82
|
43
|
+
PRK = 0x8adae09a2a307059478d309b26c4115a224cfaf6
|
44
|
+
OKM = 0x0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4
|
45
|
+
|
46
|
+
Test with SHA1 and empty salt/info
|
47
|
+
Hash = SHA1
|
48
|
+
IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
|
49
|
+
salt =
|
50
|
+
info =
|
51
|
+
L = 42
|
52
|
+
PRK = 0xda8c8a73c7fa77288ec6f5e7c297786aa0d32d01
|
53
|
+
OKM = 0x0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hkdf
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Downey
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70196819801820 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70196819801820
|
25
|
+
description: ! 'A ruby implementation of RFC5869: HMAC-based Extract-and-Expand Key
|
26
|
+
Derivation Function (HKDF). The goal of HKDF is to take some source key material
|
27
|
+
and generate suitable cryptographic keys from it.'
|
28
|
+
email:
|
29
|
+
- jdowney@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/hkdf.rb
|
35
|
+
- README.md
|
36
|
+
- spec/hkdf_spec.rb
|
37
|
+
- spec/spec_helper.rb
|
38
|
+
- spec/test_vectors.txt
|
39
|
+
homepage: http://github.com/jtdowney/hkdf
|
40
|
+
licenses: []
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 1.8.17
|
60
|
+
signing_key:
|
61
|
+
specification_version: 3
|
62
|
+
summary: HMAC-based Key Derivation Function
|
63
|
+
test_files:
|
64
|
+
- spec/hkdf_spec.rb
|
65
|
+
- spec/spec_helper.rb
|
66
|
+
- spec/test_vectors.txt
|