Skip to content

Commit

Permalink
generate key pair (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
jshawl authored Feb 7, 2024
1 parent ba84f7f commit a7dbf32
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 22 deletions.
1 change: 1 addition & 0 deletions lib/minisign.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
require 'minisign/public_key'
require 'minisign/signature'
require 'minisign/private_key'
require 'minisign/key_pair'
58 changes: 58 additions & 0 deletions lib/minisign/key_pair.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 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*')
data = "Ed#{@kdf_algorithm}B2#{@kdf_salt}#{@kdf_opslimit}#{@kdf_memlimit}#{@keynum_sk}"
Minisign::PrivateKey.new(
"untrusted comment: minisign secret key\n#{Base64.strict_encode64(data)}",
@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

# 🤷
# https://github.com/RubyCrypto/rbnacl/blob/3e8d8f8822e2b8eeba215e6be027e8ee210edfb9/lib/rbnacl/password_hash/scrypt.rb#L33-L34
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
23 changes: 1 addition & 22 deletions lib/minisign/private_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -50,27 +50,6 @@ def key_data(bytes)
[bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]]
end

# @return [String] the <kdf_output> 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*'))
Expand Down
18 changes: 18 additions & 0 deletions lib/minisign/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kdf_output> 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
13 changes: 13 additions & 0 deletions spec/minisign/key_pair_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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
File.write('test/generated/new-keypair.key', keypair.private_key)
end
end
5 changes: 5 additions & 0 deletions spec/minisign/private_key_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
1 change: 1 addition & 0 deletions spec/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ fi

test/generated/minisign -Vm test/generated/encrypted-key.txt -p test/minisign.pub || exit 1
test/generated/minisign -Vm test/generated/unencrypted-key.txt -p test/unencrypted.pub || exit 1
echo "secret password" | test/generated/minisign -Sm test/generated/.keep -s test/generated/new-keypair.key || exit 1

0 comments on commit a7dbf32

Please sign in to comment.