hkdf-1.0.0/0000755000175000017510000000000014124005465011242 5ustar rmb571rmb571hkdf-1.0.0/spec/0000755000175000017510000000000014124005465012174 5ustar rmb571rmb571hkdf-1.0.0/spec/support/0000755000175000017510000000000014124005465013710 5ustar rmb571rmb571hkdf-1.0.0/spec/support/test_vectors.rb0000644000175000017510000000132014124005465016755 0ustar rmb571rmb571# frozen_string_literal: true # :nodoc: module TestVectors module_function # :nodoc: def vectors test_parts = File.readlines("spec/fixtures/test_vectors.txt") .map(&:strip) .reject(&:empty?) .each_slice(8) test_parts.reduce({}) do |vectors, lines| name = lines.shift values = split_test_vector(lines) vectors.merge(name => values) end end # :nodoc: def split_test_vector(lines) lines.reduce({}) do |hash, line| key, value = line.split("=").map(&:strip) value ||= "" value = [value.slice(2..-1)].pack("H*") if value.start_with?("0x") hash.merge(key.to_sym => value) end end end hkdf-1.0.0/spec/spec_helper.rb0000644000175000017510000000022114124005465015005 0ustar rmb571rmb571# frozen_string_literal: true require "hkdf" require_relative "support/test_vectors" RSpec.configure do |config| config.order = "random" end hkdf-1.0.0/spec/hkdf_spec.rb0000644000175000017510000000766014124005465014460 0ustar rmb571rmb571# frozen_string_literal: true require "spec_helper" describe HKDF do subject(:hkdf) do described_class.new(source) end let(:source) { "source" } describe "initialize" do it "accepts an IO or a string as a source" do output1 = described_class.new(source).read(32) output2 = described_class.new(StringIO.new(source)).read(32) expect(output1).to eq(output2) end it "reads in an IO at a given read size" do io = instance_spy(StringIO, :io, read: nil) described_class.new(io, read_size: 1) expect(io).to have_received(:read).with(1) end it "reads in the whole IO" do hkdf1 = described_class.new(source, read_size: 1) hkdf2 = described_class.new(source) expect(hkdf1.read(32)).to eq(hkdf2.read(32)) end it "defaults the algorithm to SHA-256" do expect(described_class.new(source).algorithm).to eq("SHA256") end it "takes an optional digest algorithm" do hkdf = described_class.new("source", algorithm: "SHA1") expect(hkdf.algorithm).to eq("SHA1") end it "defaults salt to all zeros of digest length" do salt = 0.chr * 32 hkdf_salt = described_class.new(source, salt: salt) hkdf_nosalt = described_class.new(source) expect(hkdf_salt.read(32)).to eq(hkdf_nosalt.read(32)) end it "sets salt to all zeros if empty" do hkdf_blanksalt = described_class.new(source, salt: "") hkdf_nosalt = described_class.new(source) expect(hkdf_blanksalt.read(32)).to eq(hkdf_nosalt.read(32)) end it "defaults info to an empty string" do hkdf_info = described_class.new(source, info: "") hkdf_noinfo = described_class.new(source) expect(hkdf_info.read(32)).to eq(hkdf_noinfo.read(32)) end end describe "max_length" do it "is 255 times the digest length" do expect(hkdf.max_length).to eq(255 * 32) end end describe "read" do it "does not raise if reading <= max_length" do expect do hkdf.read(hkdf.max_length) end.not_to raise_error end it "raises an error if requested size is > max_length" do expect do hkdf.read(hkdf.max_length + 1) end.to 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.read(32) hkdf.read(hkdf.max_length - 31) end.to raise_error(RangeError, /requested \d+ bytes, only \d+ available/) end it "advances the stream position" do expect(hkdf.read(32)).not_to eq(hkdf.read(32)) end TestVectors.vectors.each do |name, options| it "matches output from the '#{name}' test vector" do options[:algorithm] = options[:Hash] hkdf = described_class.new(options[:IKM], options) expect(hkdf.read(options[:L].to_i)).to eq(options[:OKM]) end end end describe "read_hex" do it "returns the next bytes as hex" do expect(hkdf.read_hex(20)).to eq("fb496612b8cb82cd2297770f83c72b377af16d7b") end end describe "seek" do it "sets the position anywhere in the stream" do hkdf.read(10) output = hkdf.read(32) hkdf.seek(10) expect(hkdf.read(32)).to eq(output) end it "does not raise if <= max_length" do expect { hkdf.seek(hkdf.max_length) }.not_to raise_error end it "raises an error if requested to seek past end of stream" do expect { hkdf.seek(hkdf.max_length + 1) }.to raise_error(RangeError, /cannot seek past \d+/) end end describe "rewind" do it "resets the stream position to the beginning" do output = hkdf.read(32) hkdf.rewind expect(hkdf.read(32)).to eq(output) end end describe "inspect" do it "returns minimal information" do hkdf = described_class.new("secret", info: "public") expect(hkdf.inspect).to match(/^#$/) end end end hkdf-1.0.0/spec/fixtures/0000755000175000017510000000000014124005465014045 5ustar rmb571rmb571hkdf-1.0.0/spec/fixtures/test_vectors.txt0000644000175000017510000000625314124005465017340 0ustar rmb571rmb571Basic 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 Test with SHA-1, salt not provided (defaults to HashLen zero octets), zero-length info Hash = SHA1 IKM = 0x0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c salt = info = L = 42 PRK = 0x2adccada18779e7c2077ad2eb19d3f3e731385dd OKM = 0x2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48 hkdf-1.0.0/lib/0000755000175000017510000000000014124005465012010 5ustar rmb571rmb571hkdf-1.0.0/lib/hkdf/0000755000175000017510000000000014124005465012724 5ustar rmb571rmb571hkdf-1.0.0/lib/hkdf/version.rb0000644000175000017510000000011614124005465014734 0ustar rmb571rmb571# frozen_string_literal: true class HKDF # :nodoc: VERSION = "1.0.0" end hkdf-1.0.0/lib/hkdf.rb0000644000175000017510000000555414124005465013262 0ustar rmb571rmb571# frozen_string_literal: true require "openssl" require "stringio" # Provide HMAC-based Extract-and-Expand Key Derivation Function (HKDF) for Ruby. class HKDF # Default hash algorithm to use for HMAC. DEFAULT_ALGOTIHM = "SHA256" # Default buffer size for reading source IO. DEFAULT_READ_SIZE = 512 * 1024 # Create a new HKDF instance with then provided +source+ key material. # # Options: # - +algorithm:+ hash function to use (defaults to SHA-256) # - +info:+ optional context and application specific information # - +salt:+ optional salt value (a non-secret random value) # - +read_size:+ buffer size when reading from a source IO def initialize(source, options = {}) source = StringIO.new(source) if source.is_a?(String) algorithm = options.fetch(:algorithm, DEFAULT_ALGOTIHM) @digest = OpenSSL::Digest.new(algorithm) @info = options.fetch(:info, "") salt = options[:salt] salt = 0.chr * @digest.digest_length if salt.nil? || salt.empty? read_size = options.fetch(:read_size, DEFAULT_READ_SIZE) @prk = generate_prk(salt, source, read_size) @position = 0 @blocks = [""] end # Returns the hash algorithm this instance was configured with. def algorithm @digest.name end # Maximum length that can be derived per the RFC. def max_length @max_length ||= @digest.digest_length * 255 end # Adjust the reading position to an arbitrary offset. Will raise +RangeError+ if you attempt to seek longer than # +#max_length+. def seek(position) raise RangeError, "cannot seek past #{max_length}" if position > max_length @position = position end # Adjust reading position back to the beginning. def rewind seek(0) end # Read the next +length+ bytes from the stream. Will raise +RangeError+ if you attempt to read beyond +#max_length+. def read(length) new_position = length + @position raise RangeError, "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 # Read the next +length+ bytes from the stream and return them hex encoded. Will raise +RangeError+ if you attempt to # read beyond +#max_length+. def read_hex(length) read(length).unpack1("H*") end # :nodoc: def inspect "#{to_s[0..-2]} algorithm=#{@digest.name.inspect} info=#{@info.inspect}>" end private def generate_prk(salt, source, read_size) hmac = OpenSSL::HMAC.new(salt, @digest) while (block = source.read(read_size)) hmac.update(block) end hmac.digest 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 hkdf-1.0.0/hkdf.gemspec0000644000175000017510000000422314124005465013524 0ustar rmb571rmb571######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: hkdf 1.0.0 ruby lib Gem::Specification.new do |s| s.name = "hkdf".freeze s.version = "1.0.0" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["John Downey".freeze] s.date = "2021-04-16" s.description = "A ruby implementation of RFC5869: HMAC-based Extract-and-Expand Key Derivation Function (HKDF). The goal of HKDF is\nto take some source key material and generate suitable cryptographic keys from it.\n".freeze s.email = ["jdowney@gmail.com".freeze] s.files = ["LICENSE".freeze, "README.md".freeze, "lib/hkdf.rb".freeze, "lib/hkdf/version.rb".freeze, "spec/fixtures/test_vectors.txt".freeze, "spec/hkdf_spec.rb".freeze, "spec/spec_helper.rb".freeze, "spec/support/test_vectors.rb".freeze] s.homepage = "http://github.com/jtdowney/hkdf".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.4.0".freeze) s.rubygems_version = "3.2.5".freeze s.summary = "HMAC-based Key Derivation Function".freeze s.test_files = ["spec/fixtures/test_vectors.txt".freeze, "spec/hkdf_spec.rb".freeze, "spec/spec_helper.rb".freeze, "spec/support/test_vectors.rb".freeze] if s.respond_to? :specification_version then s.specification_version = 4 end if s.respond_to? :add_runtime_dependency then s.add_development_dependency(%q.freeze, ["~> 12.0"]) s.add_development_dependency(%q.freeze, ["~> 3.9"]) s.add_development_dependency(%q.freeze, ["~> 1.12"]) s.add_development_dependency(%q.freeze, ["~> 0.5.1"]) s.add_development_dependency(%q.freeze, ["~> 2.2"]) else s.add_dependency(%q.freeze, ["~> 12.0"]) s.add_dependency(%q.freeze, ["~> 3.9"]) s.add_dependency(%q.freeze, ["~> 1.12"]) s.add_dependency(%q.freeze, ["~> 0.5.1"]) s.add_dependency(%q.freeze, ["~> 2.2"]) end end hkdf-1.0.0/LICENSE0000644000175000017510000000203714124005465012251 0ustar rmb571rmb571Copyright (c) 2013 John Downey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. hkdf-1.0.0/README.md0000644000175000017510000000240014124005465012515 0ustar rmb571rmb571# HKDF [![CI](https://github.com/jtdowney/hkdf/actions/workflows/ci.yml/badge.svg)](https://github.com/jtdowney/hkdf/actions/workflows/ci.yml) 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. ## Usage ```ruby hkdf = HKDF.new('source key material') hkdf.read(32) => "\f#\xF4b\x98\x9B\x7Fw>|/|k\xF4k\xB7\xB9\x11e\xC5\x92\xD1\fH\xFDG\x94vt\xB4\x14\xCE" ``` The default algorithm is HMAC-SHA256, you can override this and other defaults by providing an options hash during construction. ```ruby hkdf = HKDF.new('source key material', :salt => 'NaCl', :algorithm => 'SHA1', :info => 'the 411') hkdf.read(16) => "\xC0<\x13\x85\x8C\x84z\xCE\xC7\xCE+\xFF\x1C\xEB\xE6\xBC" ``` You can also give an IO object as the source. It will be read in as a stream to generate the key. The optional argument `:read_size` can be used to control how many bytes are read from the IO at a time. ```ruby hkdf = HKDF.new(File.new('/tmp/filename'), :read_size => 512) hkdf.read(32) => "\f#\xF4b\x98\x9B\x7Fw>|/|k\xF4k\xB7\xB9\x11e\xC5\x92\xD1\fH\xFDG\x94vt\xB4\x14\xCE" ``` ## Requirements - Ruby >= 2.4