dchest / tweetnacl-js

Port of TweetNaCl cryptographic library to JavaScript
https://tweetnacl.js.org
The Unlicense
1.77k stars 293 forks source link

32 bytes secret key #166

Closed NN-Binary closed 5 years ago

NN-Binary commented 5 years ago

Hi,

We'd really want to switch on Tweetnacl but our users only saved their private key as 32 bytes (without the pubkey included).

Anyway you can please provide a small function that gives the publickey from the privatekey (for signing later on with ED25519).

Thanks for this amazing work

dchest commented 5 years ago

I generally recommend storing the seed (value before hashing and bit clearing) which was used to generate private keys, not the 32-byte part of the key itself. Then you can use nacl.sign.keypair.fromSeed to get the key pair back.

However, if you already have it stored, the following addition (modified crypto_sign_keypair) to nacl.js or nacl-fast.js should work:

// somewhere below crypto_sign_keypair:

function crypto_sign_keypair_from_sk(pk, sk) {
  var d = new Uint8Array(64);
  var p = [gf(), gf(), gf(), gf()];
  var i;

  for (i = 0; i < 32; i++) d[i] = sk[i];
  d[0] &= 248;
  d[31] &= 127;
  d[31] |= 64;

  scalarbase(p, d);
  pack(pk, p);

  return 0;
}

// and somewhere below nacl.sign.keyPair.fromSecretKey

nacl.sign.keyPair.fromPartialSecretKey = function(secretKey) {
  checkArrayTypes(secretKey);
  if (secretKey.length !== 32)
    throw new Error('bad secret key size');
  var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES);
  crypto_sign_keypair_from_sk(pk, secretKey);
  return {publicKey: pk, secretKey: new Uint8Array(secretKey)};
};

(Haven't tested this code)

I'm not sure if I'd want to add this function into the library (it adds quite a big modification to TweetNaCl internals, and this library is a port of TweetNaCl), but please leave the issue open so that we can have a vote for it.

NN-Binary commented 5 years ago

Thank you so much!

NN-Binary commented 5 years ago

I truly believe you should have the 2 options because half the libraries are 32 bytes while the remaining ones are 64 bytes. And i'm convinced lot of people can't switch on this library because of this specific reason.

NN-Binary commented 5 years ago

Your example wasn't working, I fixed it:

    function crypto_sign_keypair_from_sk(pk, sk) {

      var d = new Uint8Array(64);
      var p = [gf(), gf(), gf(), gf()];
      var i;

      for (i = 0; i < 32; i++) d[i] = sk[i];
      crypto_hash(d, sk, 32);
      d[0] &= 248;
      d[31] &= 127;
      d[31] |= 64;

      scalarbase(p, d);
      pack(pk, p);

      return 0;
    }
dchest commented 5 years ago

Ehm, this is what nacl.sign.keyPair.fromSeed does (see the internal crypto_sign_keypair implementation), so what you want then is already in TweetNaCl-js :) This is called "seed" rather than private key, because the actual private key is the hashed (and bit-cleared) version of the seed.

NN-Binary commented 5 years ago

Even a reason more I think to let people choose between 32 or 64, it means that your output (as a private key) isn't compatible with most of the libs we find online if it extract just the seed.

dchest commented 5 years ago

I'm not sure I'm following.

What you consider a 32-byte private key is in fact a seed in TweetNaCl-js terminology. The actual private key as per original Ed25519 and NaCl/TweetNaCl implementation is a concatenation of seed and public key. RFC 8032, which has 32-byte keys was released later than our releases. nacl.sign.keyPair.fromSeed was specifically introduce to accommodate such implementations. Go golang.org/x/crypto library also uses the same terminology.

var seed = window.crypto.getRandomValues(new Uint8Array(32))
console.log(seed)
var keys = nacl.sign.keyPair.fromSeed(seed)
console.log(keys.secretKey)

Output:

Uint8Array(32) [249, 226, 98, 92, 242, 5, 133, 79, 124, 20, 90, 100, 162, 149, 66, 69, 218, 126, 87, 148, 235, 194, 171, 4, 83, 203, 63, 107, 45, 168, 228, 48]
Uint8Array(64) [249, 226, 98, 92, 242, 5, 133, 79, 124, 20, 90, 100, 162, 149, 66, 69, 218, 126, 87, 148, 235, 194, 171, 4, 83, 203, 63, 107, 45, 168, 228, 48, 229, 47, 124, 229, 225, 167, 64, 216, 82, 75, 123, 136, 12, 93, 121, 65, 8, 76, 34, 251, 107, 27, 197, 81, 205, 42, 61, 13, 229, 165, 4, 34]

What's not compatible with other libs? If they use 32-byte seed as a representation of private key, it can be used with TweetNaCl-js by calling nacl.sign.keyPair.fromSeed(). If you want to use keys generated by nacl.sign.keyPair() in such libs, just use the first 32 bytes of the generated secretKey.

I think I need update readme to mention this, but otherwise, there's no incompatibility.

NN-Binary commented 5 years ago

nacl.sign.keyPair() gives out the hashed version, not the seed, am I right?

dchest commented 5 years ago

No, it gives the original random bytes.

const key1 = nacl.sign.keyPair()
const key2 = nacl.sign.keyPair.fromSeed(key1.secretKey.subarray(0, 32))
console.log(key1.secretKey);
console.log(key2.secretKey):

Output:

Uint8Array(64) [101, 5, 55, 23, 176, 97, 43, 130, 254, 211, 6, 165, 126, 114, 213, 207, 236, 250, 156, 106, 141, 110, 105, 162, 7, 102, 73, 142, 109, 102, 21, 137, 252, 6, 76, 228, 196, 168, 75, 159, 50, 130, 94, 169, 89, 74, 38, 181, 233, 211, 120, 73, 25, 243, 172, 93, 146, 74, 117, 144, 96, 244, 89, 185]
Uint8Array(64) [101, 5, 55, 23, 176, 97, 43, 130, 254, 211, 6, 165, 126, 114, 213, 207, 236, 250, 156, 106, 141, 110, 105, 162, 7, 102, 73, 142, 109, 102, 21, 137, 252, 6, 76, 228, 196, 168, 75, 159, 50, 130, 94, 169, 89, 74, 38, 181, 233, 211, 120, 73, 25, 243, 172, 93, 146, 74, 117, 144, 96, 244, 89, 185]

See implementation here: https://github.com/dchest/tweetnacl-js/blob/master/nacl.js#L704-L720

NN-Binary commented 5 years ago

Thanks for the clarification. Is it a normal thing as per ED25519 to give out the seed and not the final private key?

dchest commented 5 years ago

Yes, this was in the original implementation, and also the seed became known as the private key in RFC.