hkdf 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 [](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
|