diff --git a/lib/minisign.rb b/lib/minisign.rb index 0a12b39..dd35c34 100644 --- a/lib/minisign.rb +++ b/lib/minisign.rb @@ -8,3 +8,4 @@ require 'minisign/public_key' require 'minisign/signature' require 'minisign/private_key' +require 'minisign/key_pair' diff --git a/lib/minisign/key_pair.rb b/lib/minisign/key_pair.rb new file mode 100644 index 0000000..6528125 --- /dev/null +++ b/lib/minisign/key_pair.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Minisign + class KeyPair + include Minisign::Utils + + attr_reader :private_key + def initialize(password = nil) + key_id = SecureRandom.bytes(8) + signing_key = Ed25519::SigningKey.generate + + key_data = "Ed#{key_id}#{signing_key.to_bytes}#{signing_key.verify_key.to_bytes}" + @checksum = blake2b256(key_data) + @keynum_sk = "#{key_id}#{signing_key.to_bytes}#{signing_key.verify_key.to_bytes}#{@checksum}" + + @kdf_opslimit = [0, 0, 0, 2, 0, 0, 0, 0].pack('C*') + @kdf_memlimit = [0, 0, 0, 64, 0, 0, 0, 0].pack('C*') + @kdf_salt = SecureRandom.bytes(32) + if password + p @kdf_opslimit.unpack('N*').sum + kdf_output = derive_key( + password, + @kdf_salt, + [0, 0, 0, 2, 0, 0, 0, 0].pack("V*").unpack('N*').sum, + [0, 0, 0, 64, 0, 0, 0, 0].pack("V*").unpack('N*').sum + ) + p "kdf_output #{kdf_output}" + @keynum_sk = xor(kdf_output, @keynum_sk.bytes).pack("C*") + end + @kdf_algorithm = password.nil? ? [0, 0].pack('U*') : 'Sc' + @private_key = Minisign::PrivateKey.new( + Base64.strict_encode64("Ed#{@kdf_algorithm}B2#{@kdf_salt}#{@kdf_opslimit}#{@kdf_memlimit}#{@keynum_sk}"), + password + ) + end + end +end diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index 50fd66f..fd28832 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -27,7 +27,7 @@ def initialize(str, password = nil) @kdf_opslimit = bytes[38..45].pack('V*').unpack('N*').sum @kdf_memlimit = bytes[46..53].pack('V*').unpack('N*').sum key_data_bytes = if password - kdf_output = derive_key(password, @kdf_salt, @kdf_opslimit, @kdf_memlimit) + kdf_output = derive_key(password, @kdf_salt.pack('C*'), @kdf_opslimit, @kdf_memlimit) xor(kdf_output, bytes[54..157]) else bytes[54..157] @@ -47,27 +47,8 @@ def key_data(bytes) [bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]] end - # @return [String] the used to xor the ed25519 keys - def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit) - RbNaCl::PasswordHash.scrypt( - password, - kdf_salt.pack('C*'), - kdf_opslimit, - kdf_memlimit, - 104 - ).bytes - end - # rubocop:disable Layout/LineLength - # @return [Array<32 bit unsigned ints>] the byte array containing the key id, the secret and public ed25519 keys, and the checksum - def xor(kdf_output, contents) - # rubocop:enable Layout/LineLength - kdf_output.each_with_index.map do |b, i| - contents[i] ^ b - end - end - # @return [Ed25519::SigningKey] the ed25519 signing key def ed25519_signing_key Ed25519::SigningKey.new(@secret_key.pack('C*')) diff --git a/lib/minisign/utils.rb b/lib/minisign/utils.rb index 311b5ea..940e3f8 100644 --- a/lib/minisign/utils.rb +++ b/lib/minisign/utils.rb @@ -10,5 +10,24 @@ def blake2b256(message) def blake2b512(message) RbNaCl::Hash::Blake2b.digest(message, { digest_size: 64 }) end + + # @return [Array<32 bit unsigned ints>] the byte array containing the key id, the secret and public ed25519 keys, and the checksum + def xor(kdf_output, contents) + # rubocop:enable Layout/LineLength + kdf_output.each_with_index.map do |b, i| + contents[i] ^ b + end + end + + # @return [String] the used to xor the ed25519 keys + def derive_key(password, kdf_salt, kdf_opslimit, kdf_memlimit) + RbNaCl::PasswordHash.scrypt( + password, + kdf_salt, + kdf_opslimit, + kdf_memlimit, + 104 + ).bytes + end end end diff --git a/spec/minisign/key_pair_spec.rb b/spec/minisign/key_pair_spec.rb new file mode 100644 index 0000000..c9e4c61 --- /dev/null +++ b/spec/minisign/key_pair_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +describe Minisign::KeyPair do + it 'generates a keypair without a password' do + keypair = Minisign::KeyPair.new + expect(keypair.private_key).to be_truthy + end + it 'generates a keypair with a password' do + keypair = Minisign::KeyPair.new("secret password") + expect(keypair.private_key).to be_truthy + end +end diff --git a/spec/minisign/private_key_spec.rb b/spec/minisign/private_key_spec.rb index de55977..83e5d56 100644 --- a/spec/minisign/private_key_spec.rb +++ b/spec/minisign/private_key_spec.rb @@ -14,6 +14,11 @@ expect(@private_key.kdf_algorithm).to eq('Sc') end + it 'parses the kdf_algorithm' do + @unencrypted_private_key = Minisign::PrivateKey.new(File.read('test/unencrypted.key')) + expect(@unencrypted_private_key.kdf_algorithm.unpack('C*')).to eq([0, 0]) + end + it 'raises if the private key requires a password but is not supplied' do expect do Minisign::PrivateKey.new(File.read('test/minisign.key'))