package com.mygeopay.core.wallet;
import com.mygeopay.core.protos.Protos;
import org.bitcoinj.core.BloomFilter;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Utils;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicHierarchy;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.EncryptedData;
import org.bitcoinj.crypto.HDKeyDerivation;
import org.bitcoinj.crypto.HDUtils;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.crypto.KeyCrypterException;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.store.UnreadableWalletException;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.EncryptableKeyChain;
import org.bitcoinj.wallet.KeyBag;
import org.bitcoinj.wallet.KeyChainEventListener;
import org.bitcoinj.wallet.RedeemData;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.digests.RIPEMD160Digest;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.math.ec.ECPoint;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Lists.newLinkedList;
/**
* @author John L. Jegutanis
* @author 2013 The bitcoinj developers.
*/
public class SimpleHDKeyChain implements EncryptableKeyChain, KeyBag {
private static final Logger log = LoggerFactory.getLogger(SimpleHDKeyChain.class);
public static final int LOOKAHEAD = 20; // BIP 44
private final ReentrantLock lock = Threading.lock("KeyChain");
private DeterministicHierarchy hierarchy;
private DeterministicKey rootKey;
// Paths through the key tree. External keys are ones that are communicated to other parties. Internal keys are
// keys created for change addresses, coinbases, mixing, etc - anything that isn't communicated. The distinction
// is somewhat arbitrary but can be useful for audits.
public static final ChildNumber EXTERNAL_PATH_NUM = ChildNumber.ZERO;
public static final ChildNumber INTERNAL_PATH_NUM = ChildNumber.ONE;
public static final ImmutableList<ChildNumber> EXTERNAL_PATH = ImmutableList.of(EXTERNAL_PATH_NUM);
public static final ImmutableList<ChildNumber> INTERNAL_PATH = ImmutableList.of(INTERNAL_PATH_NUM);
// We try to ensure we have at least this many keys ready and waiting to be handed out via getKey().
// See docs for getLookaheadSize() for more info on what this is for. The -1 value means it hasn't been calculated
// yet. For new chains it's set to whatever the default is, unless overridden by setLookaheadSize. For deserialized
// chains, it will be calculated on demand from the number of loaded keys.
private static final int LAZY_CALCULATE_LOOKAHEAD = -1;
private int lookaheadSize = LOOKAHEAD;
// The lookahead threshold causes us to batch up creation of new keys to minimize the frequency of Bloom filter
// regenerations, which are expensive and will (in future) trigger chain download stalls/retries. One third
// is an efficiency tradeoff.
private int lookaheadThreshold = calcDefaultLookaheadThreshold();
private int calcDefaultLookaheadThreshold() {
return lookaheadSize / 3;
}
// The parent keys for external keys (handed out to other people) and internal keys (used for change addresses).
private DeterministicKey externalKey, internalKey;
// How many keys on each path have actually been used. This may be fewer than the number that have been deserialized
// or held in memory, because of the lookahead zone.
private int issuedExternalKeys, issuedInternalKeys;
// We simplify by wrapping a basic key chain and that way we get some functionality like key lookup and event
// listeners "for free". All keys in the key tree appear here, even if they aren't meant to be used for receiving
// money.
private final SimpleKeyChain simpleKeyChain;
/**
* Creates a deterministic key chain that watches the given (public only) root key. You can use this to calculate
* balances and generally follow along, but spending is not possible with such a chain. Currently you can't use
* this method to watch an arbitrary fragment of some other tree, this limitation may be removed in future.
*/
public SimpleHDKeyChain(DeterministicKey rootkey) {
simpleKeyChain = new SimpleKeyChain();
initializeHierarchyUnencrypted(rootkey);
}
SimpleHDKeyChain(DeterministicKey rootkey, @Nullable KeyCrypter crypter) {
this.rootKey = rootkey;
simpleKeyChain = new SimpleKeyChain(crypter);
if (!rootkey.isEncrypted()) {
initializeHierarchyUnencrypted(rootKey);
}
// Else...
// We can't initialize ourselves with just an encrypted seed, so we expected deserialization code to do the
// rest of the setup (loading the root key).
}
SimpleHDKeyChain(DeterministicKey rootkey, @Nullable KeyCrypter crypter,
@Nullable KeyParameter key) {
simpleKeyChain = new SimpleKeyChain(crypter);
if (crypter != null && !rootkey.isEncrypted()) {
this.rootKey = rootkey.encrypt(crypter, key, null);
} else {
this.rootKey = rootkey;
}
initializeHierarchyUnencrypted(rootKey);
}
// For use in encryption.
private SimpleHDKeyChain(KeyCrypter crypter, KeyParameter aesKey, SimpleHDKeyChain chain) {
checkArgument(!chain.rootKey.isEncrypted(), "Chain already encrypted");
this.issuedExternalKeys = chain.issuedExternalKeys;
this.issuedInternalKeys = chain.issuedInternalKeys;
this.lookaheadSize = chain.lookaheadSize;
this.lookaheadThreshold = chain.lookaheadThreshold;
simpleKeyChain = new SimpleKeyChain(crypter);
// The first number is the "account number" but we don't use that feature.
rootKey = chain.rootKey.encrypt(crypter, aesKey, null);
hierarchy = new DeterministicHierarchy(rootKey);
simpleKeyChain.importKey(rootKey);
externalKey = encryptNonLeaf(aesKey, chain, rootKey, EXTERNAL_PATH);
internalKey = encryptNonLeaf(aesKey, chain, rootKey, INTERNAL_PATH);
// Now copy the (pubkey only) leaf keys across to avoid rederiving them. The private key bytes are missing
// anyway so there's nothing to encrypt.
for (ECKey eckey : chain.simpleKeyChain.getKeys()) {
DeterministicKey key = (DeterministicKey) eckey;
if (!isLeaf(key)) continue; // Not a leaf key.
DeterministicKey parent = hierarchy.get(checkNotNull(key.getParent(), "Key has no parent").getPath(), false, false);
// Clone the key to the new encrypted hierarchy.
key = new DeterministicKey(key.getPubOnly(), parent);
hierarchy.putKey(key);
simpleKeyChain.importKey(key);
}
}
private DeterministicKey encryptNonLeaf(KeyParameter aesKey, SimpleHDKeyChain chain,
DeterministicKey parent, ImmutableList<ChildNumber> path) {
DeterministicKey key = chain.hierarchy.get(path, true, false);
key = key.encrypt(checkNotNull(simpleKeyChain.getKeyCrypter(), "Chain has null KeyCrypter"), aesKey, parent);
hierarchy.putKey(key);
simpleKeyChain.importKey(key);
return key;
}
// Derives the account path keys and inserts them into the basic key chain. This is important to preserve their
// order for serialization, amongst other things.
private void initializeHierarchyUnencrypted(DeterministicKey baseKey) {
rootKey = baseKey;
addToBasicChain(rootKey);
hierarchy = new DeterministicHierarchy(rootKey);
externalKey = hierarchy.get(EXTERNAL_PATH, true, true);
internalKey = hierarchy.get(INTERNAL_PATH, true, true);
addToBasicChain(externalKey);
addToBasicChain(internalKey);
}
public boolean isEncrypted() {
lock.lock();
try {
return rootKey.isEncrypted();
}
finally {
lock.unlock();
}
}
/**
* Get an unused key, without actually issuing it.
* This is useful to show a receiving key in the interface
*/
public DeterministicKey getCurrentUnusedKey(KeyPurpose purpose) {
lock.lock();
try {
List<DeterministicKey> keys = null;
switch (purpose) {
case RECEIVE_FUNDS:
case REFUND:
keys = getDeterministicKeys(1, externalKey, issuedExternalKeys + 1);
break;
case CHANGE:
keys = getDeterministicKeys(1, internalKey, issuedInternalKeys + 1);
break;
default:
throw new UnsupportedOperationException();
}
return keys.get(0);
} finally {
lock.unlock();
}
}
/**
* Get the last issued key
*/
@Nullable
public DeterministicKey getLastIssuedKey(KeyPurpose purpose) {
lock.lock();
try {
List<DeterministicKey> keys;
switch (purpose) {
case RECEIVE_FUNDS:
case REFUND:
if (issuedExternalKeys <= 0) return null;
keys = getDeterministicKeys(1, externalKey, issuedExternalKeys);
break;
case CHANGE:
if (issuedInternalKeys <= 0) return null;
keys = getDeterministicKeys(1, internalKey, issuedInternalKeys);
break;
default:
throw new UnsupportedOperationException();
}
return keys.get(0);
} finally {
lock.unlock();
}
}
/** Returns a freshly derived key that has not been returned by this method before. */
@Override
public DeterministicKey getKey(KeyPurpose purpose) {
return getKeys(purpose, 1).get(0);
}
/** Returns freshly derived key/s that have not been returned by this method before. */
@Override
public List<DeterministicKey> getKeys(KeyPurpose purpose, int numberOfKeys) {
checkArgument(numberOfKeys > 0, "Need at least 1 key");
lock.lock();
try {
DeterministicKey parentKey;
int index;
switch (purpose) {
// Map both REFUND and RECEIVE_KEYS to the same branch for now. Refunds are a feature of the BIP 70
// payment protocol. Later we may wish to map it to a different branch (in a new wallet version?).
// This would allow a watching wallet to only be able to see inbound payments, but not change
// (i.e. spends) or refunds. Might be useful for auditing ...
case RECEIVE_FUNDS:
case REFUND:
issuedExternalKeys += numberOfKeys;
index = issuedExternalKeys;
parentKey = externalKey;
break;
case CHANGE:
issuedInternalKeys += numberOfKeys;
index = issuedInternalKeys;
parentKey = internalKey;
break;
default:
throw new UnsupportedOperationException();
}
List<DeterministicKey> keys = getDeterministicKeys(numberOfKeys, parentKey, index);
return keys;
} finally {
lock.unlock();
}
}
private List<DeterministicKey> getDeterministicKeys(int numberOfKeys, DeterministicKey parentKey, int index) {
lock.lock();
try {
// Optimization: potentially do a very quick key generation for just the number of keys we need if we
// didn't already create them, ignoring the configured lookahead size. This ensures we'll be able to
// retrieve the keys in the following loop, but if we're totally fresh and didn't get a chance to
// calculate the lookahead keys yet, this will not block waiting to calculate 100+ EC point multiplies.
// On slow/crappy Android phones looking ahead 100 keys can take ~5 seconds but the OS will kill us
// if we block for just one second on the UI thread. Because UI threads may need an address in order
// to render the screen, we need getKeys to be fast even if the wallet is totally brand new and lookahead
// didn't happen yet.
//
// It's safe to do this because when a network thread tries to calculate a Bloom filter, we'll go ahead
// and calculate the full lookahead zone there, so network requests will always use the right amount.
List<DeterministicKey> lookahead = maybeLookAhead(parentKey, index, 0, 0);
simpleKeyChain.importKeys(lookahead);
List<DeterministicKey> keys = new ArrayList<DeterministicKey>(numberOfKeys);
for (int i = 0; i < numberOfKeys; i++) {
ImmutableList<ChildNumber> path = HDUtils.append(parentKey.getPath(),
new ChildNumber(index - numberOfKeys + i, false));
keys.add(hierarchy.get(path, false, false));
}
return keys;
} finally {
lock.unlock();
}
}
private void addToBasicChain(DeterministicKey key) {
simpleKeyChain.importKeys(ImmutableList.of(key));
}
/**
* Mark the DeterministicKey as used.
* Also correct the issued{Internal|External}Keys counter, because all lower children seem to be requested already.
* If the counter was updated, we also might trigger lookahead.
*/
public DeterministicKey markKeyAsUsed(DeterministicKey k) {
int numChildren = k.getChildNumber().i() + 1;
if (k.getParent() == internalKey) {
if (issuedInternalKeys < numChildren) {
issuedInternalKeys = numChildren;
maybeLookAhead();
}
} else if (k.getParent() == externalKey) {
if (issuedExternalKeys < numChildren) {
issuedExternalKeys = numChildren;
maybeLookAhead();
}
}
return k;
}
@Override
public DeterministicKey findKeyFromPubHash(byte[] pubkeyHash) {
lock.lock();
try {
return (DeterministicKey) simpleKeyChain.findKeyFromPubHash(pubkeyHash);
} finally {
lock.unlock();
}
}
@Override
public DeterministicKey findKeyFromPubKey(byte[] pubkey) {
lock.lock();
try {
return (DeterministicKey) simpleKeyChain.findKeyFromPubKey(pubkey);
} finally {
lock.unlock();
}
}
@Nullable
@Override
public RedeemData findRedeemDataFromScriptHash(byte[] bytes) {
log.warn("Method findRedeemDataFromScriptHash not implemented");
return null;
}
/**
* Mark the DeterministicKeys as used, if they match the pubkeyHash
* See {@link SimpleHDKeyChain#markKeyAsUsed(DeterministicKey)} for more info on this.
*/
public boolean markPubHashAsUsed(byte[] pubkeyHash) {
lock.lock();
try {
DeterministicKey k = (DeterministicKey) simpleKeyChain.findKeyFromPubHash(pubkeyHash);
if (k != null)
markKeyAsUsed(k);
return k != null;
} finally {
lock.unlock();
}
}
/**
* Mark the DeterministicKeys as used, if they match the pubkey
* See {@link SimpleHDKeyChain#markKeyAsUsed(DeterministicKey)} for more info on this.
*/
public boolean markPubKeyAsUsed(byte[] pubkey) {
lock.lock();
try {
DeterministicKey k = (DeterministicKey) simpleKeyChain.findKeyFromPubKey(pubkey);
if (k != null)
markKeyAsUsed(k);
return k != null;
} finally {
lock.unlock();
}
}
@Override
public boolean hasKey(ECKey key) {
lock.lock();
try {
return simpleKeyChain.hasKey(key);
} finally {
lock.unlock();
}
}
/** Returns the deterministic key for the given absolute path in the hierarchy. */
protected DeterministicKey getKeyByPath(ChildNumber... path) {
return getKeyByPath(ImmutableList.<ChildNumber>copyOf(path));
}
/** Returns the deterministic key for the given absolute path in the hierarchy. */
protected DeterministicKey getKeyByPath(ImmutableList<ChildNumber> path) {
return hierarchy.get(path, false, false);
}
/**
* <p>Use this when you would like to create a watching key chain that follows this one,
* but can't spend money from it.</p>
*/
public DeterministicKey getWatchingKey() {
return rootKey.getPubOnly();
}
@Override
public int numKeys() {
lock.lock();
try {
maybeLookAhead();
return simpleKeyChain.numKeys();
} finally {
lock.unlock();
}
}
/**
* Returns number of leaf keys used including both internal and external paths. This may be fewer than the number
* that have been deserialized or held in memory, because of the lookahead zone.
*/
public int numLeafKeysIssued() {
lock.lock();
try {
return issuedExternalKeys + issuedInternalKeys;
} finally {
lock.unlock();
}
}
@Override
public long getEarliestKeyCreationTime() {
return rootKey.getCreationTimeSeconds();
}
@Override
public void addEventListener(KeyChainEventListener listener) {
simpleKeyChain.addEventListener(listener);
}
@Override
public void addEventListener(KeyChainEventListener listener, Executor executor) {
simpleKeyChain.addEventListener(listener, executor);
}
@Override
public boolean removeEventListener(KeyChainEventListener listener) {
return simpleKeyChain.removeEventListener(listener);
}
/**
* Return true if this keychain is following another keychain
*/
public boolean isFollowing() {
return false; // No support for now
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Serialization support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<Protos.Key> toProtobuf() {
LinkedList<Protos.Key> entries = newLinkedList();
List<Protos.Key.Builder> protos = toEditableProtobuf();
for (Protos.Key.Builder proto : protos) {
entries.add(proto.build());
}
return entries;
}
List<Protos.Key.Builder> toEditableProtobuf() {
lock.lock();
try {
// Most of the serialization work is delegated to the basic key chain, which will serialize the bulk of the
// data (handling encryption along the way), and letting us patch it up with the extra data we care about.
LinkedList<Protos.Key.Builder> entries = newLinkedList();
Map<ECKey, Protos.Key.Builder> keys = simpleKeyChain.toEditableProtobufs();
for (Map.Entry<ECKey, Protos.Key.Builder> entry : keys.entrySet()) {
DeterministicKey key = (DeterministicKey) entry.getKey();
Protos.Key.Builder proto = entry.getValue();
proto.setType(Protos.Key.Type.DETERMINISTIC_KEY);
final Protos.DeterministicKey.Builder detKey = proto.getDeterministicKeyBuilder();
detKey.setChainCode(ByteString.copyFrom(key.getChainCode()));
for (ChildNumber num : key.getPath())
detKey.addPath(num.i());
if (key.equals(externalKey)) {
detKey.setIssuedSubkeys(issuedExternalKeys);
detKey.setLookaheadSize(lookaheadSize);
} else if (key.equals(internalKey)) {
detKey.setIssuedSubkeys(issuedInternalKeys);
detKey.setLookaheadSize(lookaheadSize);
}
// Flag the very first key of following keychain.
if (entries.isEmpty() && isFollowing()) {
detKey.setIsFollowing(true);
}
entries.add(proto);
}
return entries;
} finally {
lock.unlock();
}
}
/**
* Returns the key chain found in the given list of keys. Used for unencrypted chains
*/
public static SimpleHDKeyChain fromProtobuf(List<Protos.Key> keys) throws UnreadableWalletException {
return fromProtobuf(keys, null);
}
/**
* Returns the key chain found in the given list of keys.
*/
public static SimpleHDKeyChain fromProtobuf(List<Protos.Key> keys, @Nullable KeyCrypter crypter) throws UnreadableWalletException {
SimpleHDKeyChain chain = null;
int lookaheadSize = -1;
// If the root key is a child of another hierarchy, the depth will be > 0
int rootTreeSize = 0;
for (Protos.Key key : keys) {
final Protos.Key.Type t = key.getType();
if (t == Protos.Key.Type.DETERMINISTIC_KEY) {
if (!key.hasDeterministicKey())
throw new UnreadableWalletException("Deterministic key missing extra data: " + key.toString());
if (chain == null) {
DeterministicKey rootKey = getDeterministicKey(key, null, crypter);
chain = new SimpleHDKeyChain(rootKey, crypter);
chain.lookaheadSize = LAZY_CALCULATE_LOOKAHEAD;
rootTreeSize = rootKey.getPath().size();
}
LinkedList<ChildNumber> path = newLinkedList(getKeyProtoPath(key));
// Find the parent key assuming this is not the root key, and not an account key for a watching chain.
DeterministicKey parent = null;
if (path.size() > rootTreeSize) {
ChildNumber index = path.removeLast();
parent = chain.hierarchy.get(path, false, false);
path.add(index);
}
DeterministicKey detkey = getDeterministicKey(key, parent, crypter);
if (log.isDebugEnabled()) {
log.debug("Deserializing: DETERMINISTIC_KEY: {}", detkey);
}
// If the non-encrypted case, the non-leaf keys (account, internal, external) have already been
// rederived and inserted at this point and the two lines below are just a no-op. In the encrypted
// case though, we can't rederive and we must reinsert, potentially building the heirarchy object
// if need be.
if (path.size() == rootTreeSize) {
// Master key.
chain.rootKey = detkey;
chain.hierarchy = new DeterministicHierarchy(detkey);
} else if (path.size() == rootTreeSize + EXTERNAL_PATH.size()) {
if (EXTERNAL_PATH_NUM.equals(detkey.getChildNumber())) {
chain.externalKey = detkey;
chain.issuedExternalKeys = key.getDeterministicKey().getIssuedSubkeys();
lookaheadSize = Math.max(lookaheadSize, key.getDeterministicKey().getLookaheadSize());
} else if (INTERNAL_PATH_NUM.equals(detkey.getChildNumber())) {
chain.internalKey = detkey;
chain.issuedInternalKeys = key.getDeterministicKey().getIssuedSubkeys();
}
}
chain.hierarchy.putKey(detkey);
chain.simpleKeyChain.importKey(detkey);
}
}
if (chain == null) {
throw new UnreadableWalletException("Could not create a key chain.");
}
checkState(lookaheadSize >= 0, "Negative lookahead size");
chain.setLookaheadSize(lookaheadSize);
chain.maybeLookAhead();
return chain;
}
public static DeterministicKey getDeterministicKey(Protos.Key key,
@Nullable DeterministicKey parent,
@Nullable KeyCrypter crypter) {
// Deserialize the path through the tree.
final ImmutableList<ChildNumber> immutablePath = getKeyProtoPath(key);
// Deserialize the public key.
ECPoint pubkey = ECKey.CURVE.getCurve().decodePoint(key.getPublicKey().toByteArray());
// Deserialize the chain code.
byte[] chainCode = key.getDeterministicKey().getChainCode().toByteArray();
DeterministicKey detkey;
if (key.hasSecretBytes()) {
// Not encrypted: private key is available.
final BigInteger priv = new BigInteger(1, key.getSecretBytes().toByteArray());
detkey = new DeterministicKey(immutablePath, chainCode, pubkey, priv, parent);
} else {
if (key.hasEncryptedData()) {
Protos.EncryptedData proto = key.getEncryptedData();
EncryptedData data = new EncryptedData(proto.getInitialisationVector().toByteArray(),
proto.getEncryptedPrivateKey().toByteArray());
checkNotNull(crypter, "Encountered an encrypted key but no key crypter provided");
detkey = new DeterministicKey(immutablePath, chainCode, crypter, pubkey, data, parent);
} else {
// No secret key bytes and key is not encrypted: either a watching key or private key bytes
// will be rederived on the fly from the parent.
checkNotNull(parent, "Watching keys are not supported at the moment.");
detkey = new DeterministicKey(immutablePath, chainCode, pubkey, null, parent);
}
}
return detkey;
}
private static ImmutableList<ChildNumber> getKeyProtoPath(Protos.Key key) {
ImmutableList.Builder<ChildNumber> pathBuilder = ImmutableList.builder();
for (int i : key.getDeterministicKey().getPathList()) {
pathBuilder.add(new ChildNumber(i));
}
return pathBuilder.build();
}
@Override
public List<org.bitcoinj.wallet.Protos.Key> serializeToProtobuf() {
throw new RuntimeException("Not implemented. Use HDKeyChain.toProtobuf() instead.");
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Encryption support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public SimpleHDKeyChain toEncrypted(CharSequence password) {
checkNotNull(password, "Attempt to encrypt with a null password.");
checkArgument(password.length() > 0, "Attempt to encrypt with an empty password.");
checkState(!rootKey.isEncrypted(), "Attempt to encrypt a root key that is already encrypted.");
checkState(!rootKey.isPubKeyOnly(), "Attempt to encrypt a watching chain.");
KeyCrypter scrypt = new KeyCrypterScrypt();
KeyParameter derivedKey = scrypt.deriveKey(password);
return toEncrypted(scrypt, derivedKey);
}
@Override
public SimpleHDKeyChain toEncrypted(KeyCrypter keyCrypter, KeyParameter aesKey) {
return new SimpleHDKeyChain(keyCrypter, aesKey, this);
}
@Override
public SimpleHDKeyChain toDecrypted(CharSequence password) {
checkNotNull(password, "Attempt to decrypt with a null password.");
checkArgument(password.length() > 0, "Attempt to decrypt with an empty password.");
KeyCrypter crypter = getKeyCrypter();
checkState(crypter != null, "Chain not encrypted");
KeyParameter derivedKey = crypter.deriveKey(password);
return toDecrypted(derivedKey);
}
@Override
public SimpleHDKeyChain toDecrypted(KeyParameter aesKey) {
checkState(getKeyCrypter() != null, "Key chain not encrypted");
checkState(rootKey.isEncrypted(), "Root key not encrypted");
DeterministicKey decKey = rootKey.decrypt(getKeyCrypter(), aesKey);
SimpleHDKeyChain chain = new SimpleHDKeyChain(decKey);
// Now double check that the keys match to catch the case where the key is wrong but padding didn't catch it.
if (!chain.getWatchingKey().getPubKeyPoint().equals(getWatchingKey().getPubKeyPoint()))
throw new KeyCrypterException("Provided AES key is wrong");
chain.lookaheadSize = lookaheadSize;
// Now copy the (pubkey only) leaf keys across to avoid rederiving them. The private key bytes are missing
// anyway so there's nothing to decrypt.
for (ECKey eckey : simpleKeyChain.getKeys()) {
DeterministicKey key = (DeterministicKey) eckey;
if (!isLeaf(key)) continue; // Not a leaf key.
checkState(key.isEncrypted(), "Key is not encrypted");
DeterministicKey parent = chain.hierarchy.get(checkNotNull(key.getParent(), "Key has null parent").getPath(), false, false);
// Clone the key to the new decrypted hierarchy.
key = new DeterministicKey(key.getPubOnly(), parent);
chain.hierarchy.putKey(key);
chain.simpleKeyChain.importKeys(key);
}
chain.issuedExternalKeys = issuedExternalKeys;
chain.issuedInternalKeys = issuedInternalKeys;
return chain;
}
private boolean isLeaf(DeterministicKey key) {
return key.getPath().size() > internalKey.getPath().size();
}
@Override
public boolean checkPassword(CharSequence password) {
checkNotNull(password,"Password is null");
checkState(getKeyCrypter() != null, "Key chain not encrypted");
return checkAESKey(getKeyCrypter().deriveKey(password));
}
@Override
public boolean checkAESKey(KeyParameter aesKey) {
checkNotNull(aesKey, "Cannot check null KeyParameter");
checkState(getKeyCrypter() != null, "Key chain not encrypted");
try {
return rootKey.decrypt(aesKey).getPubKeyPoint().equals(rootKey.getPubKeyPoint());
} catch (KeyCrypterException e) {
return false;
}
}
@Nullable
@Override
public KeyCrypter getKeyCrypter() {
return simpleKeyChain.getKeyCrypter();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Bloom filtering support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int numBloomFilterEntries() {
return numKeys() * 2;
}
@Override
public BloomFilter getFilter(int size, double falsePositiveRate, long tweak) {
lock.lock();
try {
checkArgument(size >= numBloomFilterEntries(), "Bloom filter too small");
maybeLookAhead();
return simpleKeyChain.getFilter(size, falsePositiveRate, tweak);
} finally {
lock.unlock();
}
}
/**
* <p>The number of public keys we should pre-generate on each path before they are requested by the app. This is
* required so that when scanning through the chain given only a seed, we can give enough keys to the remote node
* via the Bloom filter such that we see transactions that are "from the future", for example transactions created
* by a different app that's sharing the same seed, or transactions we made before but we're replaying the chain
* given just the seed. The default is 100.</p>
*/
public int getLookaheadSize() {
lock.lock();
try {
return lookaheadSize;
} finally {
lock.unlock();
}
}
/**
* Sets a new lookahead size. See {@link #getLookaheadSize()} for details on what this is. Setting a new size
* that's larger than the current size will return immediately and the new size will only take effect next time
* a fresh filter is requested (e.g. due to a new peer being connected). So you should set this before starting
* to sync the chain, if you want to modify it. If you haven't modified the lookahead threshold manually then
* it will be automatically set to be a third of the new size.
*/
public void setLookaheadSize(int lookaheadSize) {
lock.lock();
try {
boolean readjustThreshold = this.lookaheadThreshold == calcDefaultLookaheadThreshold();
this.lookaheadSize = lookaheadSize;
if (readjustThreshold)
this.lookaheadThreshold = calcDefaultLookaheadThreshold();
} finally {
lock.unlock();
}
}
/**
* Sets the threshold for the key pre-generation.
* If a key is used in a transaction, the keychain would pre-generate a new key, for every issued key,
* even if it is only one. If the blockchain is replayed, every key would trigger a regeneration
* of the bloom filter sent to the peers as a consequence.
* To prevent this, new keys are only generated, if more than the threshold value are needed.
*/
public void setLookaheadThreshold(int num) {
lock.lock();
try {
if (num >= lookaheadSize)
throw new IllegalArgumentException("Threshold larger or equal to the lookaheadSize");
this.lookaheadThreshold = num;
} finally {
lock.unlock();
}
}
/**
* Gets the threshold for the key pre-generation.
* See {@link #setLookaheadThreshold(int)} for details on what this is.
*/
public int getLookaheadThreshold() {
lock.lock();
try {
if (lookaheadThreshold >= lookaheadSize)
return 0;
return lookaheadThreshold;
} finally {
lock.unlock();
}
}
/**
* Pre-generate enough keys to reach the lookahead size. You can call this if you need to explicitly invoke
* the lookahead procedure, but it's normally unnecessary as it will be done automatically when needed.
*/
public void maybeLookAhead() {
lock.lock();
try {
List<DeterministicKey> keys = maybeLookAhead(externalKey, issuedExternalKeys);
keys.addAll(maybeLookAhead(internalKey, issuedInternalKeys));
// Batch add all keys at once so there's only one event listener invocation, as this will be listened to
// by the wallet and used to rebuild/broadcast the Bloom filter. That's expensive so we don't want to do
// it more often than necessary.
simpleKeyChain.importKeys(keys);
} finally {
lock.unlock();
}
}
private List<DeterministicKey> maybeLookAhead(DeterministicKey parent, int issued) {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
return maybeLookAhead(parent, issued, getLookaheadSize(), getLookaheadThreshold());
}
/**
* Pre-generate enough keys to reach the lookahead size, but only if there are more than the lookaheadThreshold to
* be generated, so that the Bloom filter does not have to be regenerated that often.
*
* The returned mutable list of keys must be inserted into the basic key chain.
*/
private List<DeterministicKey> maybeLookAhead(DeterministicKey parent, int issued, int lookaheadSize, int lookaheadThreshold) {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
final int numChildren = hierarchy.getNumChildren(parent.getPath());
final int needed = issued + lookaheadSize + lookaheadThreshold - numChildren;
if (needed <= lookaheadThreshold)
return new ArrayList<DeterministicKey>();
log.info("{} keys needed for {} = {} issued + {} lookahead size + {} lookahead threshold - {} num children",
needed, parent.getPathAsString(), issued, lookaheadSize, lookaheadThreshold, numChildren);
List<DeterministicKey> result = new ArrayList<DeterministicKey>(needed);
long now = System.currentTimeMillis();
int nextChild = numChildren;
for (int i = 0; i < needed; i++) {
DeterministicKey key = HDKeyDerivation.deriveThisOrNextChildKey(parent, nextChild);
key = key.getPubOnly();
hierarchy.putKey(key);
result.add(key);
nextChild = key.getChildNumber().num() + 1;
}
log.info("Took {} msec", System.currentTimeMillis() - now);
return result;
}
/**
* Returns keys used on external path. This may be fewer than the number that have been deserialized
* or held in memory, because of the lookahead zone.
*/
public ArrayList<DeterministicKey> getIssuedExternalKeys() {
lock.lock();
try {
maybeLookAhead();
int treeSize = externalKey.getPath().size();
ArrayList<DeterministicKey> issuedKeys = new ArrayList<DeterministicKey>();
for (ECKey key : simpleKeyChain.getKeys()) {
DeterministicKey detkey = (DeterministicKey) key;
DeterministicKey parent = detkey.getParent();
if (parent == null) continue;
if (detkey.getPath().size() <= treeSize) continue;
if (parent.equals(internalKey)) continue;
if (parent.equals(externalKey) && detkey.getChildNumber().num() >= issuedExternalKeys) continue;
issuedKeys.add(detkey);
}
return issuedKeys;
} finally {
lock.unlock();
}
}
/**
* Returns number of keys used on external path. This may be fewer than the number that have been deserialized
* or held in memory, because of the lookahead zone.
*/
public int getNumIssuedExternalKeys() {
lock.lock();
try {
return issuedExternalKeys;
} finally {
lock.unlock();
}
}
/**
* Returns number of keys used on internal path. This may be fewer than the number that have been deserialized
* or held in memory, because of the lookahead zone.
*/
public int getNumIssuedInternalKeys() {
lock.lock();
try {
return issuedInternalKeys;
} finally {
lock.unlock();
}
}
// For internal usage only
/* package */ List<ECKey> getKeys(boolean includeLookahead) {
maybeLookAhead();
List<ECKey> keys = simpleKeyChain.getKeys();
if (!includeLookahead) {
int treeSize = internalKey.getPath().size();
List<ECKey> issuedKeys = new LinkedList<ECKey>();
for (ECKey key : keys) {
DeterministicKey detkey = (DeterministicKey) key;
DeterministicKey parent = detkey.getParent();
if (parent == null) continue;
if (detkey.getPath().size() <= treeSize) continue;
if (parent.equals(internalKey) && detkey.getChildNumber().num() > issuedInternalKeys) continue;
if (parent.equals(externalKey) && detkey.getChildNumber().num() > issuedExternalKeys) continue;
issuedKeys.add(detkey);
}
return issuedKeys;
}
return keys;
}
/**
* Returns leaf keys issued by this chain (including lookahead zone but no lookahead threshold)
*/
public List<DeterministicKey> getActiveKeys() {
ImmutableList.Builder<DeterministicKey> keys = ImmutableList.builder();
for (ECKey key : getKeys(true)) {
DeterministicKey dKey = (DeterministicKey) key;
if (isLeaf(dKey)) {
if (dKey.getParent().equals(internalKey) && dKey.getChildNumber().num() >= issuedInternalKeys + lookaheadSize) continue;
if (dKey.getParent().equals(externalKey) && dKey.getChildNumber().num() >= issuedExternalKeys + lookaheadSize) continue;
keys.add(dKey);
}
}
return keys.build();
}
public boolean isExternal(DeterministicKey key) {
return key.getParent() != null && key.getParent().equals(externalKey);
}
public int getAccountIndex() {
return rootKey.getChildNumber().num();
}
public String getId() {
return getId("");
}
public String getId(String salt) {
try {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
sha256.update(salt.getBytes());
byte[] hash = sha256.digest(rootKey.getPubKey());
return Utils.HEX.encode(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
}