mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 10:06:45 +00:00
private key derivation
This commit is contained in:
parent
c5042cf130
commit
d394c25a3c
4 changed files with 135 additions and 7 deletions
|
@ -13,8 +13,8 @@ import java.util.List;
|
||||||
public class DeterministicKey extends ECKey {
|
public class DeterministicKey extends ECKey {
|
||||||
private final DeterministicKey parent;
|
private final DeterministicKey parent;
|
||||||
private final List<ChildNumber> childNumberPath;
|
private final List<ChildNumber> childNumberPath;
|
||||||
private final int depth;
|
private int depth;
|
||||||
private final byte[] parentFingerprint; // 0 if this key is root node of key hierarchy
|
private byte[] parentFingerprint; // 0 if this key is root node of key hierarchy
|
||||||
|
|
||||||
/** 32 bytes */
|
/** 32 bytes */
|
||||||
private final byte[] chainCode;
|
private final byte[] chainCode;
|
||||||
|
@ -43,8 +43,9 @@ public class DeterministicKey extends ECKey {
|
||||||
public DeterministicKey(List<ChildNumber> childNumberPath,
|
public DeterministicKey(List<ChildNumber> childNumberPath,
|
||||||
byte[] chainCode,
|
byte[] chainCode,
|
||||||
LazyECPoint publicAsPoint,
|
LazyECPoint publicAsPoint,
|
||||||
|
BigInteger priv,
|
||||||
DeterministicKey parent) {
|
DeterministicKey parent) {
|
||||||
super(null, compressPoint(publicAsPoint));
|
super(priv, compressPoint(publicAsPoint));
|
||||||
if(chainCode.length != 32) {
|
if(chainCode.length != 32) {
|
||||||
throw new IllegalArgumentException("Chaincode not 32 bytes in length");
|
throw new IllegalArgumentException("Chaincode not 32 bytes in length");
|
||||||
}
|
}
|
||||||
|
@ -101,6 +102,54 @@ public class DeterministicKey extends ECKey {
|
||||||
return parent;
|
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()} */
|
/** Returns the last element of the path returned by {@link DeterministicKey#getPath()} */
|
||||||
public ChildNumber getChildNumber() {
|
public ChildNumber getChildNumber() {
|
||||||
return childNumberPath.size() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1);
|
return childNumberPath.size() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1);
|
||||||
|
|
|
@ -39,13 +39,57 @@ public class HDKeyDerivation {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
|
public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
|
||||||
RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber);
|
if(parent.isPubKeyOnly()) {
|
||||||
return new DeterministicKey(Utils.appendChild(parent.getPath(), childNumber), rawKey.chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), rawKey.keyBytes), parent);
|
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 {
|
public static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
|
||||||
if(childNumber.isHardened()) {
|
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);
|
byte[] parentPublicKey = parent.getPubKeyPoint().getEncoded(true);
|
||||||
|
@ -58,15 +102,21 @@ public class HDKeyDerivation {
|
||||||
data.putInt(childNumber.i());
|
data.putInt(childNumber.i());
|
||||||
byte[] i = Utils.getHmacSha512Hash(parent.getChainCode(), data.array());
|
byte[] i = Utils.getHmacSha512Hash(parent.getChainCode(), data.array());
|
||||||
if(i.length != 64) {
|
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[] il = Arrays.copyOfRange(i, 0, 32);
|
||||||
byte[] chainCode = Arrays.copyOfRange(i, 32, 64);
|
byte[] chainCode = Arrays.copyOfRange(i, 32, 64);
|
||||||
BigInteger ilInt = new BigInteger(1, il);
|
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();
|
final BigInteger N = ECKey.CURVE.getN();
|
||||||
ECPoint Ki = ECKey.publicPointFromPrivate(ilInt).add(parent.getPubKeyPoint());
|
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);
|
return new RawKeyBytes(Ki.getEncoded(true), chainCode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
import com.sparrowwallet.drongo.crypto.DeterministicKey;
|
import com.sparrowwallet.drongo.crypto.DeterministicKey;
|
||||||
import com.sparrowwallet.drongo.crypto.HDKeyDerivation;
|
import com.sparrowwallet.drongo.crypto.HDKeyDerivation;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class Keystore {
|
public class Keystore {
|
||||||
public static final String DEFAULT_LABEL = "Keystore 1";
|
public static final String DEFAULT_LABEL = "Keystore 1";
|
||||||
|
|
||||||
|
@ -119,4 +121,22 @@ public class Keystore {
|
||||||
}
|
}
|
||||||
return copy;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
@ -20,4 +21,12 @@ public class KeystoreTest {
|
||||||
|
|
||||||
Assert.assertEquals("xprv9s21ZrQH143K4AkrxAyivDeTCWhZV6fdLfBRR8QerWe9hHiRqMjBMj9MFNef7oFufgcDcW54LhguPNm6MVLEMWPX5qxKhmNjCzi9Zy6yhkc", keystore.getExtendedPrivateKey().toString());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue