diff --git a/.rspec b/.rspec index c99d2e7..5be63fc 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,2 @@ --require spec_helper +--format documentation diff --git a/.rubocop.yml b/.rubocop.yml index 38ba3e2..6a4ec2e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,6 @@ Metrics/BlockLength: - IgnoredMethods: ['describe', 'context'] + AllowedMethods: ['describe', 'context'] AllCops: NewCops: enable TargetRubyVersion: 2.7.7 + SuggestExtensions: false diff --git a/lib/minisign/private_key.rb b/lib/minisign/private_key.rb index 99099ba..048886e 100644 --- a/lib/minisign/private_key.rb +++ b/lib/minisign/private_key.rb @@ -4,60 +4,45 @@ module Minisign # Parse ed25519 signing key from minisign private key class PrivateKey include Utils - attr_reader :signature_algorithm, :kdf_algorithm, :cksum_algorithm, :kdf_salt, :kdf_opslimit, :kdf_memlimit, - :key_id, :ed25519_public_key, :secret_key, :checksum + attr_reader :kdf_salt, :kdf_opslimit, :kdf_memlimit, + :key_id, :ed25519_public_key_bytes, :ed25519_private_key_bytes, :checksum - # rubocop:disable Metrics/AbcSize # rubocop:disable Layout/LineLength - # rubocop:disable Metrics/MethodLength # Parse signing information from the minisign private key # # @param str [String] The minisign private key # @example - # Minisign::PrivateKey.new('RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=', 'password') + # Minisign::PrivateKey.new( + # 'RWRTY0IyEf+yYa5eAX38PgdrI3TMxwy+3sgzpgcZWQXhOKqdf9sAAAACAAAAAAAAAEAAAAAAHe8Olzttgk6k5pZyT3CyCTcTAV0bLN3kq5CUqhLjqSdYZ6oEWs/S7ztaephS+/jwnuOElLBKkg3Sd56jzyvMwL4qStNUTyPDqckNjniw2SlowmHN8n5NnR47gvqjo96E+vakpw8v5PE=', + # 'password' + # ) def initialize(str, password = nil) - contents = str.split("\n") - decoded = Base64.decode64(contents.last) - @untrusted_comment = contents.first.split('untrusted comment: ').last - bytes = decoded.bytes - @signature_algorithm, @kdf_algorithm, @cksum_algorithm = - [bytes[0..1], bytes[2..3], bytes[4..5]].map { |a| a.pack('U*') } - raise 'Missing password for encrypted key' if @kdf_algorithm.bytes.sum != 0 && password.nil? - - @kdf_salt = bytes[6..37] - @kdf_opslimit = bytes[38..45].pack('V*').unpack('N*').sum - @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.pack('C*'), @kdf_opslimit, @kdf_memlimit) - xor(kdf_output, bytes[54..157]) - else - bytes[54..157] - end - @key_id, @secret_key, @ed25519_public_key, @checksum = key_data(@key_data_bytes) - assert_keypair_match! + comment, data = str.split("\n") + @password = password + decoded = Base64.decode64(data) + @untrusted_comment = comment.split('untrusted comment: ').last + @bytes = decoded.bytes + @kdf_salt, @kdf_opslimit, @kdf_memlimit = scrypt_params(@bytes) + @key_id, @ed25519_private_key_bytes, @ed25519_public_key_bytes, @checksum = key_data(password, @bytes[54..157]) + validate_key! end # rubocop:enable Layout/LineLength - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength - # @raise [RuntimeError] if the extracted public key does not match the derived public key - def assert_keypair_match! - raise 'Wrong password for that key' if @ed25519_public_key != ed25519_signing_key.verify_key.to_bytes.bytes + def signature_algorithm + @bytes[0..1].pack('U*') end - def key_data(bytes) - [bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]] + def kdf_algorithm + @bytes[2..3].pack('U*') end - # @return [Ed25519::SigningKey] the ed25519 signing key - def ed25519_signing_key - Ed25519::SigningKey.new(@secret_key.pack('C*')) + def cksum_algorithm + @bytes[4..5].pack('U*') end def public_key - data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{ed25519_signing_key.verify_key.to_bytes}") + data = Base64.strict_encode64("Ed#{@key_id.pack('C*')}#{@ed25519_public_key_bytes.pack('C*')}") Minisign::PublicKey.new(data) end @@ -85,8 +70,35 @@ def to_s kdf_salt = @kdf_salt.pack('C*') kdf_opslimit = [@kdf_opslimit, 0].pack('L*') kdf_memlimit = [@kdf_memlimit, 0].pack('L*') - data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{@keynum_sk}" + keynum_sk = key_data(@password, + @key_id + @ed25519_private_key_bytes + @ed25519_public_key_bytes + @checksum).flatten + data = "Ed#{kdf_algorithm}B2#{kdf_salt}#{kdf_opslimit}#{kdf_memlimit}#{keynum_sk.pack('C*')}" "untrusted comment: #{@untrusted_comment}\n#{Base64.strict_encode64(data)}\n" end + + private + + def scrypt_params(bytes) + [bytes[6..37], bytes[38..45].pack('V*').unpack('N*').sum, bytes[46..53].pack('V*').unpack('N*').sum] + end + + # @raise [RuntimeError] if the extracted public key does not match the derived public key + def validate_key! + raise 'Missing password for encrypted key' if kdf_algorithm.bytes.sum != 0 && @password.nil? + raise 'Wrong password for that key' if @ed25519_public_key_bytes != ed25519_signing_key.verify_key.to_bytes.bytes + end + + def key_data(password, bytes) + if password + kdf_output = derive_key(password, @kdf_salt.pack('C*'), @kdf_opslimit, @kdf_memlimit) + bytes = xor(kdf_output, bytes) + end + [bytes[0..7], bytes[8..39], bytes[40..71], bytes[72..103]] + end + + # @return [Ed25519::SigningKey] the ed25519 signing key + def ed25519_signing_key + Ed25519::SigningKey.new(@ed25519_private_key_bytes.pack('C*')) + end end end diff --git a/spec/minisign/private_key_spec.rb b/spec/minisign/private_key_spec.rb index d689960..7613d5e 100644 --- a/spec/minisign/private_key_spec.rb +++ b/spec/minisign/private_key_spec.rb @@ -53,14 +53,15 @@ end it 'parses the public key' do - key = @private_key.ed25519_public_key + key = @private_key.ed25519_public_key_bytes expect(key).to eq([108, 35, 192, 26, 47, 128, 233, 165, 133, 38, 242, 5, 76, 55, 135, 40, 103, 72, 230, 43, 184, 117, 219, 37, 173, 250, 196, 122, 252, 174, 173, 140]) end it 'parses the secret key' do - expect(@private_key.secret_key).to eq([65, 87, 110, 33, 168, 130, 118, 100, 249, 200, 160, 167, 47, 59, 141, - 122, 156, 38, 80, 199, 139, 1, 21, 18, 116, 110, 204, 131, 199, 202, 181, 87]) # rubocop:disable Layout/LineLength + key = @private_key.ed25519_private_key_bytes + expect(key).to eq([65, 87, 110, 33, 168, 130, 118, 100, 249, 200, 160, 167, 47, 59, 141, + 122, 156, 38, 80, 199, 139, 1, 21, 18, 116, 110, 204, 131, 199, 202, 181, 87]) end it 'parses the checksum' do @@ -70,8 +71,8 @@ key_data = [ [69, 100], @private_key.key_id, - @private_key.secret_key, - @private_key.ed25519_public_key + @private_key.ed25519_private_key_bytes, + @private_key.ed25519_public_key_bytes ].inject(&:+).pack('C*') computed_checksum = blake2b256(key_data).bytes