From 2bb8b9930e54756d8c1ced3bd4a9ff88cf41a33c Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Sun, 4 Feb 2024 14:18:15 -0600 Subject: [PATCH] generate key pair --- lib/minisign.rb | 1 + lib/minisign/key_pair.rb | 55 +++++++++++++++++++++++++++++++ lib/minisign/private_key.rb | 23 +------------ lib/minisign/utils.rb | 18 ++++++++++ spec/minisign/key_pair_spec.rb | 12 +++++++ spec/minisign/private_key_spec.rb | 5 +++ 6 files changed, 92 insertions(+), 22 deletions(-) create mode 100644 lib/minisign/key_pair.rb create mode 100644 spec/minisign/key_pair_spec.rb 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..3c6ccfd --- /dev/null +++ b/lib/minisign/key_pair.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Minisign + # Generate a Minisign secret and public key + class KeyPair + include Minisign::Utils + + def initialize(password = nil) + @password = password + + kd = key_data + + @checksum = blake2b256("Ed#{kd}") + @keynum_sk = "#{kd}#{@checksum}" + + @kdf_salt = SecureRandom.bytes(32) + @keynum_sk = xor(kdf_output, @keynum_sk.bytes).pack('C*') if @password + @kdf_algorithm = password.nil? ? [0, 0].pack('U*') : 'Sc' + end + + def private_key + @kdf_opslimit = kdf_opslimit_bytes.pack('C*') + @kdf_memlimit = kdf_memlimit_bytes.pack('C*') + Minisign::PrivateKey.new( + Base64.strict_encode64("Ed#{@kdf_algorithm}B2#{@kdf_salt}#{@kdf_opslimit}#{@kdf_memlimit}#{@keynum_sk}"), + @password + ) + end + + private + + def kdf_output + derive_key( + @password, + @kdf_salt, + kdf_opslimit_bytes.pack('V*').unpack('N*').sum, + kdf_memlimit_bytes.pack('V*').unpack('N*').sum + ) + end + + def key_data + key_id = SecureRandom.bytes(8) + signing_key = Ed25519::SigningKey.generate + "#{key_id}#{signing_key.to_bytes}#{signing_key.verify_key.to_bytes}" + end + + def kdf_opslimit_bytes + [0, 0, 0, 2, 0, 0, 0, 0] + end + + def kdf_memlimit_bytes + [0, 0, 0, 64, 0, 0, 0, 0] + end + end +end diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index 023b051..124bce4 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -30,7 +30,7 @@ def initialize(str, password = nil) @kdf_memlimit = bytes[46..53].pack('V*').unpack('N*').sum @keynum_sk = bytes[54..157].pack('C*') @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] @@ -50,27 +50,6 @@ 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..8ca056c 100644 --- a/lib/minisign/utils.rb +++ b/lib/minisign/utils.rb @@ -10,5 +10,23 @@ def blake2b256(message) def blake2b512(message) RbNaCl::Hash::Blake2b.digest(message, { digest_size: 64 }) end + + # @return [Array<32 bit unsigned ints>] + def xor(kdf_output, contents) + 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..0d9662a --- /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 55a7a6c..afb0a51 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'))