diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicKey.java b/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicKey.java index b66aae3..37d663b 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicKey.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/DeterministicKey.java @@ -13,8 +13,8 @@ import java.util.List; public class DeterministicKey extends ECKey { private final DeterministicKey parent; private final List 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 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); + } + } + + /** + *

Returns the same key with the parent pointer removed (it still knows its own path and the parent fingerprint).

+ * + *

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.

+ */ + 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); diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/HDKeyDerivation.java b/src/main/java/com/sparrowwallet/drongo/crypto/HDKeyDerivation.java index eae9f0d..f926dee 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/HDKeyDerivation.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/HDKeyDerivation.java @@ -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); } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java index 6879bec..5dd83f8 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java @@ -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 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; + } } diff --git a/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java b/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java index 20979f1..82edab2 100644 --- a/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java +++ b/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java @@ -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()); + } }