private DeterministicKey encryptNonLeaf( KeyParameter aesKey, DeterministicKeyChain chain, DeterministicKey parent, ImmutableList<ChildNumber> path) { DeterministicKey key = chain.hierarchy.get(path, false, false); key = key.encrypt(checkNotNull(basicKeyChain.getKeyCrypter()), aesKey, parent); hierarchy.putKey(key); basicKeyChain.importKey(key); return key; }
@Override public DeterministicKeyChain toDecrypted(KeyParameter aesKey) { checkState(getKeyCrypter() != null, "Key chain not encrypted"); checkState(seed != null, "Can't decrypt a watching chain"); checkState(seed.isEncrypted()); String passphrase = DEFAULT_PASSPHRASE_FOR_MNEMONIC; // FIXME allow non-empty passphrase DeterministicSeed decSeed = seed.decrypt(getKeyCrypter(), passphrase, aesKey); DeterministicKeyChain chain = new DeterministicKeyChain(decSeed); // 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 : basicKeyChain.getKeys()) { DeterministicKey key = (DeterministicKey) eckey; if (key.getPath().size() != 3) continue; // Not a leaf key. checkState(key.isEncrypted()); DeterministicKey parent = chain.hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false); // Clone the key to the new decrypted hierarchy. key = new DeterministicKey(key.getPubOnly(), parent); chain.hierarchy.putKey(key); chain.basicKeyChain.importKey(key); } chain.issuedExternalKeys = issuedExternalKeys; chain.issuedInternalKeys = issuedInternalKeys; return chain; }
public DeterministicKey findKeyFromPubKey(byte[] pubkey) { lock.lock(); try { return (DeterministicKey) basicKeyChain.findKeyFromPubKey(pubkey); } finally { lock.unlock(); } }
@Override public List<Protos.Key> serializeToProtobuf() { 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> entries = newLinkedList(); if (seed != null) { Protos.Key.Builder mnemonicEntry = BasicKeyChain.serializeEncryptableItem(seed); mnemonicEntry.setType(Protos.Key.Type.DETERMINISTIC_MNEMONIC); entries.add(mnemonicEntry.build()); } Map<ECKey, Protos.Key.Builder> keys = basicKeyChain.serializeToEditableProtobufs(); 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); } if (key.getParent() != null) { // HD keys inherit the timestamp of their parent if they have one, so no need to serialize // it. proto.clearCreationTimestamp(); } entries.add(proto.build()); } return entries; } finally { lock.unlock(); } }
@Override public boolean hasKey(ECKey key) { lock.lock(); try { return basicKeyChain.hasKey(key); } finally { lock.unlock(); } }
@Override public BloomFilter getFilter(int size, double falsePositiveRate, long tweak) { lock.lock(); try { checkArgument(size >= numBloomFilterEntries()); maybeLookAhead(); return basicKeyChain.getFilter(size, falsePositiveRate, tweak); } finally { lock.unlock(); } }
/** * Mark the DeterministicKeys as used, if they match the pubkey See {@link * com.google.bitcoin.wallet.DeterministicKeyChain#markKeyAsUsed(DeterministicKey)} for more info * on this. */ @Nullable public DeterministicKey markPubKeyAsUsed(byte[] pubkey) { lock.lock(); try { DeterministicKey k = (DeterministicKey) basicKeyChain.findKeyFromPubKey(pubkey); if (k != null) markKeyAsUsed(k); return k; } finally { lock.unlock(); } }
// For use in encryption. private DeterministicKeyChain( KeyCrypter crypter, KeyParameter aesKey, DeterministicKeyChain chain) { // Can't encrypt a watching chain. checkNotNull(chain.rootKey); checkNotNull(chain.seed); checkArgument(!chain.rootKey.isEncrypted(), "Chain already encrypted"); this.issuedExternalKeys = chain.issuedExternalKeys; this.issuedInternalKeys = chain.issuedInternalKeys; this.lookaheadSize = chain.lookaheadSize; this.lookaheadThreshold = chain.lookaheadThreshold; this.seed = chain.seed.encrypt(crypter, aesKey); basicKeyChain = new BasicKeyChain(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); basicKeyChain.importKey(rootKey); DeterministicKey account = encryptNonLeaf(aesKey, chain, rootKey, ACCOUNT_ZERO_PATH); externalKey = encryptNonLeaf(aesKey, chain, account, EXTERNAL_PATH); internalKey = encryptNonLeaf(aesKey, chain, account, 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.basicKeyChain.getKeys()) { DeterministicKey key = (DeterministicKey) eckey; if (key.getPath().size() != 3) continue; // Not a leaf key. DeterministicKey parent = hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false); // Clone the key to the new encrypted hierarchy. key = new DeterministicKey(key.getPubOnly(), parent); hierarchy.putKey(key); basicKeyChain.importKey(key); } }
@Override public int numKeys() { // We need to return here the total number of keys including the lookahead zone, not the number // of keys we // have issued via getKey/freshReceiveKey. lock.lock(); try { maybeLookAhead(); return basicKeyChain.numKeys(); } 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. basicKeyChain.importKeys(keys); } finally { lock.unlock(); } }
// For internal usage only /* package */ List<ECKey> getKeys(boolean includeLookahead) { List<ECKey> keys = basicKeyChain.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().i() > issuedInternalKeys) continue; if (parent.equals(externalKey) && detkey.getChildNumber().i() > issuedExternalKeys) continue; issuedKeys.add(detkey); } return issuedKeys; } return keys; }
@Nullable @Override public KeyCrypter getKeyCrypter() { return basicKeyChain.getKeyCrypter(); }
/** 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); 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 AUTHENTICATION: case CHANGE: issuedInternalKeys += numberOfKeys; index = issuedInternalKeys; parentKey = internalKey; break; default: throw new UnsupportedOperationException(); } // 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); basicKeyChain.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)); DeterministicKey k = hierarchy.get(path, false, false); // Just a last minute sanity check before we hand the key out to the app for usage. This // isn't inspired // by any real problem reports from bitcoinj users, but I've heard of cases via the // grapevine of // places that lost money due to bitflips causing addresses to not match keys. Of course in // an // environment with flaky RAM there's no real way to always win: bitflips could be // introduced at any // other layer. But as we're potentially retrieving from long term storage here, check // anyway. checkForBitFlip(k); keys.add(k); } return keys; } finally { lock.unlock(); } }
@Override public boolean removeEventListener(KeyChainEventListener listener) { return basicKeyChain.removeEventListener(listener); }
@Override public void addEventListener(KeyChainEventListener listener, Executor executor) { basicKeyChain.addEventListener(listener, executor); }
@Override public void addEventListener(KeyChainEventListener listener) { basicKeyChain.addEventListener(listener); }
private void addToBasicChain(DeterministicKey key) { basicKeyChain.importKeys(ImmutableList.of(key)); }