private key derivation

This commit is contained in:
Craig Raw 2020-05-07 14:36:03 +02:00
parent c5042cf130
commit d394c25a3c
4 changed files with 135 additions and 7 deletions

View file

@ -13,8 +13,8 @@ import java.util.List;
public class DeterministicKey extends ECKey {
private final DeterministicKey parent;
private final List<ChildNumber> childNumberPath;
private final int depth;
private final byte[] parentFingerprint; // 0 if this key is root node of key hierarchy
private int depth;
private byte[] parentFingerprint; // 0 if this key is root node of key hierarchy
/** 32 bytes */
private final byte[] chainCode;
@ -43,8 +43,9 @@ public class DeterministicKey extends ECKey {
public DeterministicKey(List<ChildNumber> childNumberPath,
byte[] chainCode,
LazyECPoint publicAsPoint,
BigInteger priv,
DeterministicKey parent) {
super(null, compressPoint(publicAsPoint));
super(priv, compressPoint(publicAsPoint));
if(chainCode.length != 32) {
throw new IllegalArgumentException("Chaincode not 32 bytes in length");
}
@ -101,6 +102,54 @@ public class DeterministicKey extends ECKey {
return parent;
}
/**
* Return the fingerprint of the key from which this key was derived, if this is a
* child key, or else an array of four zero-value bytes.
*/
public byte[] getParentFingerprint() {
return parentFingerprint;
}
/**
* Returns private key bytes, padded with zeros to 33 bytes.
* @throws java.lang.IllegalStateException if the private key bytes are missing.
*/
public byte[] getPrivKeyBytes33() {
byte[] bytes33 = new byte[33];
byte[] priv = getPrivKeyBytes();
System.arraycopy(priv, 0, bytes33, 33 - priv.length, priv.length);
return bytes33;
}
/**
* Returns the same key with the private bytes removed. May return the same instance. The purpose of this is to save
* memory: the private key can always be very efficiently rederived from a parent that a private key, so storing
* all the private keys in RAM is a poor tradeoff especially on constrained devices. This means that the returned
* key may still be usable for signing and so on, so don't expect it to be a true pubkey-only object! If you want
* that then you should follow this call with a call to {@link #dropParent()}.
*/
public DeterministicKey dropPrivateBytes() {
if (isPubKeyOnly()) {
return this;
} else {
return new DeterministicKey(getPath(), getChainCode(), pub, null, parent);
}
}
/**
* <p>Returns the same key with the parent pointer removed (it still knows its own path and the parent fingerprint).</p>
*
* <p>If this key doesn't have private key bytes stored/cached itself, but could rederive them from the parent, then
* the new key returned by this method won't be able to do that. Thus, using dropPrivateBytes().dropParent() on a
* regular DeterministicKey will yield a new DeterministicKey that cannot sign or do other things involving the
* private key at all.</p>
*/
public DeterministicKey dropParent() {
DeterministicKey key = new DeterministicKey(getPath(), getChainCode(), pub, priv, null);
key.parentFingerprint = parentFingerprint;
key.depth = depth;
return key;
}
/** Returns the last element of the path returned by {@link DeterministicKey#getPath()} */
public ChildNumber getChildNumber() {
return childNumberPath.size() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1);

View file

@ -39,13 +39,57 @@ public class HDKeyDerivation {
}
public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber);
return new DeterministicKey(Utils.appendChild(parent.getPath(), childNumber), rawKey.chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), rawKey.keyBytes), parent);
if(parent.isPubKeyOnly()) {
RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber);
return new DeterministicKey(Utils.appendChild(parent.getPath(), childNumber), rawKey.chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), rawKey.keyBytes), null, parent);
} else {
RawKeyBytes rawKey = deriveChildKeyBytesFromPrivate(parent, childNumber);
return new DeterministicKey(Utils.appendChild(parent.getPath(), childNumber), rawKey.chainCode, new BigInteger(1, rawKey.keyBytes), parent);
}
}
public static RawKeyBytes deriveChildKeyBytesFromPrivate(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
if(parent.isPubKeyOnly()) {
throw new HDDerivationException("Parent key must have private key bytes for this method");
}
byte[] parentPublicKey = parent.getPubKeyPoint().getEncoded(true);
if(parentPublicKey.length != 33) {
throw new HDDerivationException("Parent pubkey must be 33 bytes, but is " + parentPublicKey.length);
}
ByteBuffer data = ByteBuffer.allocate(37);
if (childNumber.isHardened()) {
data.put(parent.getPrivKeyBytes33());
} else {
data.put(parentPublicKey);
}
data.putInt(childNumber.i());
byte[] i = Utils.getHmacSha512Hash(parent.getChainCode(), data.array());
if(i.length != 64) {
throw new HDDerivationException("HmacSHA512 output must be 64 bytes, is " + i.length);
}
byte[] il = Arrays.copyOfRange(i, 0, 32);
byte[] chainCode = Arrays.copyOfRange(i, 32, 64);
BigInteger ilInt = new BigInteger(1, il);
if(ilInt.compareTo(ECKey.CURVE.getN()) > 0) {
throw new HDDerivationException("Illegal derived key: I_L >= n");
}
final BigInteger priv = parent.getPrivKey();
BigInteger ki = priv.add(ilInt).mod(ECKey.CURVE.getN());
if(ki.equals(BigInteger.ZERO)) {
throw new HDDerivationException("Illegal derived key: derived private key equals 0");
}
return new RawKeyBytes(ki.toByteArray(), chainCode);
}
public static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
if(childNumber.isHardened()) {
throw new HDDerivationException("Can't use private derivation with public keys only.");
throw new HDDerivationException("Can't use private derivation with public keys only");
}
byte[] parentPublicKey = parent.getPubKeyPoint().getEncoded(true);
@ -58,15 +102,21 @@ public class HDKeyDerivation {
data.putInt(childNumber.i());
byte[] i = Utils.getHmacSha512Hash(parent.getChainCode(), data.array());
if(i.length != 64) {
throw new HDDerivationException("HmacSHA512 output must be 64 bytes, is" + i.length);
throw new HDDerivationException("HmacSHA512 output must be 64 bytes, is " + i.length);
}
byte[] il = Arrays.copyOfRange(i, 0, 32);
byte[] chainCode = Arrays.copyOfRange(i, 32, 64);
BigInteger ilInt = new BigInteger(1, il);
if(ilInt.compareTo(ECKey.CURVE.getN()) > 0) {
throw new HDDerivationException("Illegal derived key: I_L >= n");
}
final BigInteger N = ECKey.CURVE.getN();
ECPoint Ki = ECKey.publicPointFromPrivate(ilInt).add(parent.getPubKeyPoint());
if(Ki.equals(ECKey.CURVE.getCurve().getInfinity())) {
throw new HDDerivationException("Illegal derived key: derived public key equals infinity");
}
return new RawKeyBytes(Ki.getEncoded(true), chainCode);
}

View file

@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.DeterministicKey;
import com.sparrowwallet.drongo.crypto.HDKeyDerivation;
import java.util.List;
public class Keystore {
public static final String DEFAULT_LABEL = "Keystore 1";
@ -119,4 +121,22 @@ public class Keystore {
}
return copy;
}
public static Keystore fromSeed(byte[] seed, List<ChildNumber> derivation) {
Keystore keystore = new Keystore();
keystore.setSeed(seed);
ExtendedKey xprv = keystore.getExtendedPrivateKey();
String masterFingerprint = Utils.bytesToHex(xprv.getKey().getFingerprint());
DeterministicKey derivedKey = xprv.getKey(derivation);
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1));
keystore.setLabel(masterFingerprint);
keystore.setSource(KeystoreSource.SW_SEED);
keystore.setWalletModel(WalletModel.SPARROW);
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation)));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString()));
return keystore;
}
}

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.ScriptType;
import org.junit.Assert;
import org.junit.Test;
@ -20,4 +21,12 @@ public class KeystoreTest {
Assert.assertEquals("xprv9s21ZrQH143K4AkrxAyivDeTCWhZV6fdLfBRR8QerWe9hHiRqMjBMj9MFNef7oFufgcDcW54LhguPNm6MVLEMWPX5qxKhmNjCzi9Zy6yhkc", keystore.getExtendedPrivateKey().toString());
}
@Test
public void testFromSeed() {
ScriptType p2pkh = ScriptType.P2PKH;
Keystore keystore = Keystore.fromSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c"), p2pkh.getDefaultDerivation());
Assert.assertEquals("xpub6DCH2YkjweBu5zQheCWgSu6o26AENhApkS2taXaJBsi6vthRytPTaY2Sh4zDHj7oCVhYxx5974HbSbKxh26ah7N6VVw1U8kS2H5HfPUXecq", keystore.getExtendedPublicKey().toString());
}
}