/* * Copyright 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.bitcoinj.wallet; import org.bitcoinj.core.*; import org.bitcoinj.crypto.*; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.UnitTestParams; import org.bitcoinj.utils.BriefLogFormatter; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.listeners.AbstractKeyChainEventListener; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; import org.spongycastle.crypto.params.KeyParameter; import java.io.IOException; import java.security.SecureRandom; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; import static org.junit.Assert.*; public class DeterministicKeyChainTest { private DeterministicKeyChain chain; private final byte[] ENTROPY = Sha256Hash.hash("don't use a string seed like this in real life".getBytes()); @Before public void setup() { BriefLogFormatter.init(); // You should use a random seed instead. The secs constant comes from the unit test file, so we can compare // serialized data properly. long secs = 1389353062L; chain = new DeterministicKeyChain(ENTROPY, "", secs); chain.setLookaheadSize(10); assertEquals(secs, checkNotNull(chain.getSeed()).getCreationTimeSeconds()); } @Test public void derive() throws Exception { ECKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertFalse(key1.isPubKeyOnly()); ECKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertFalse(key2.isPubKeyOnly()); final Address address = Address.fromBase58(UnitTestParams.get(), "n1bQNoEx8uhmCzzA5JPG6sFdtsUQhwiQJV"); assertEquals(address, key1.toAddress(UnitTestParams.get())); assertEquals("mnHUcqUVvrfi5kAaXJDQzBb9HsWs78b42R", key2.toAddress(UnitTestParams.get()).toString()); assertEquals(key1, chain.findKeyFromPubHash(address.getHash160())); assertEquals(key2, chain.findKeyFromPubKey(key2.getPubKey())); key1.sign(Sha256Hash.ZERO_HASH); assertFalse(key1.isPubKeyOnly()); ECKey key3 = chain.getKey(KeyChain.KeyPurpose.CHANGE); assertFalse(key3.isPubKeyOnly()); assertEquals("mqumHgVDqNzuXNrszBmi7A2UpmwaPMx4HQ", key3.toAddress(UnitTestParams.get()).toString()); key3.sign(Sha256Hash.ZERO_HASH); assertFalse(key3.isPubKeyOnly()); } @Test public void getKeys() throws Exception { chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); chain.getKey(KeyChain.KeyPurpose.CHANGE); chain.maybeLookAhead(); assertEquals(2, chain.getKeys(false, false).size()); } @Test public void deriveAccountOne() throws Exception { long secs = 1389353062L; DeterministicKeyChain chain1 = new AccountOneChain(ENTROPY, "", secs); ECKey key1 = chain1.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); ECKey key2 = chain1.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); final Address address = Address.fromBase58(UnitTestParams.get(), "n2nHHRHs7TiZScTuVhZUkzZfTfVgGYwy6X"); assertEquals(address, key1.toAddress(UnitTestParams.get())); assertEquals("mnp2j9za5zMuz44vNxrJCXXhZsCdh89QXn", key2.toAddress(UnitTestParams.get()).toString()); assertEquals(key1, chain1.findKeyFromPubHash(address.getHash160())); assertEquals(key2, chain1.findKeyFromPubKey(key2.getPubKey())); key1.sign(Sha256Hash.ZERO_HASH); ECKey key3 = chain1.getKey(KeyChain.KeyPurpose.CHANGE); assertEquals("mpjRhk13rvV7vmnszcUQVYVQzy4HLTPTQU", key3.toAddress(UnitTestParams.get()).toString()); key3.sign(Sha256Hash.ZERO_HASH); } static class AccountOneChain extends DeterministicKeyChain { public AccountOneChain(byte[] entropy, String s, long secs) { super(entropy, s, secs); } public AccountOneChain(KeyCrypter crypter, DeterministicSeed seed) { super(seed, crypter); } @Override protected ImmutableList<ChildNumber> getAccountPath() { return ImmutableList.of(ChildNumber.ONE); } } @Test public void serializeAccountOne() throws Exception { long secs = 1389353062L; DeterministicKeyChain chain1 = new AccountOneChain(ENTROPY, "", secs); ECKey key1 = chain1.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); final Address address = Address.fromBase58(UnitTestParams.get(), "n2nHHRHs7TiZScTuVhZUkzZfTfVgGYwy6X"); assertEquals(address, key1.toAddress(UnitTestParams.get())); DeterministicKey watching = chain1.getWatchingKey(); List<Protos.Key> keys = chain1.serializeToProtobuf(); KeyChainFactory factory = new KeyChainFactory() { @Override public DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed, KeyCrypter crypter, boolean isMarried) { return new AccountOneChain(crypter, seed); } @Override public DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, boolean isFollowingKey, boolean isMarried) { throw new UnsupportedOperationException(); } }; chain1 = DeterministicKeyChain.fromProtobuf(keys, null, factory).get(0); ECKey key2 = chain1.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertEquals("mnp2j9za5zMuz44vNxrJCXXhZsCdh89QXn", key2.toAddress(UnitTestParams.get()).toString()); assertEquals(key1, chain1.findKeyFromPubHash(address.getHash160())); assertEquals(key2, chain1.findKeyFromPubKey(key2.getPubKey())); key1.sign(Sha256Hash.ZERO_HASH); ECKey key3 = chain1.getKey(KeyChain.KeyPurpose.CHANGE); assertEquals("mpjRhk13rvV7vmnszcUQVYVQzy4HLTPTQU", key3.toAddress(UnitTestParams.get()).toString()); key3.sign(Sha256Hash.ZERO_HASH); assertEquals(watching, chain1.getWatchingKey()); } @Test public void signMessage() throws Exception { ECKey key = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); key.verifyMessage("test", key.signMessage("test")); } @Test public void events() throws Exception { // Check that we get the right events at the right time. final List<List<ECKey>> listenerKeys = Lists.newArrayList(); long secs = 1389353062L; chain = new DeterministicKeyChain(ENTROPY, "", secs); chain.addEventListener(new AbstractKeyChainEventListener() { @Override public void onKeysAdded(List<ECKey> keys) { listenerKeys.add(keys); } }, Threading.SAME_THREAD); assertEquals(0, listenerKeys.size()); chain.setLookaheadSize(5); assertEquals(0, listenerKeys.size()); ECKey key = chain.getKey(KeyChain.KeyPurpose.CHANGE); assertEquals(1, listenerKeys.size()); // 1 event final List<ECKey> firstEvent = listenerKeys.get(0); assertEquals(1, firstEvent.size()); assertTrue(firstEvent.contains(key)); // order is not specified. listenerKeys.clear(); chain.maybeLookAhead(); final List<ECKey> secondEvent = listenerKeys.get(0); assertEquals(12, secondEvent.size()); // (5 lookahead keys, +1 lookahead threshold) * 2 chains listenerKeys.clear(); chain.getKey(KeyChain.KeyPurpose.CHANGE); // At this point we've entered the threshold zone so more keys won't immediately trigger more generations. assertEquals(0, listenerKeys.size()); // 1 event final int lookaheadThreshold = chain.getLookaheadThreshold() + chain.getLookaheadSize(); for (int i = 0; i < lookaheadThreshold; i++) chain.getKey(KeyChain.KeyPurpose.CHANGE); assertEquals(1, listenerKeys.size()); // 1 event assertEquals(1, listenerKeys.get(0).size()); // 1 key. } @Test public void random() { // Can't test much here but verify the constructor worked and the class is functional. The other tests rely on // a fixed seed to be deterministic. chain = new DeterministicKeyChain(new SecureRandom(), 384); chain.setLookaheadSize(10); chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).sign(Sha256Hash.ZERO_HASH); chain.getKey(KeyChain.KeyPurpose.CHANGE).sign(Sha256Hash.ZERO_HASH); } @Test public void serializeUnencrypted() throws UnreadableWalletException { chain.maybeLookAhead(); DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key3 = chain.getKey(KeyChain.KeyPurpose.CHANGE); List<Protos.Key> keys = chain.serializeToProtobuf(); // 1 mnemonic/seed, 1 master key, 1 account key, 2 internal keys, 3 derived, 20 lookahead and 5 lookahead threshold. int numItems = 1 // mnemonic/seed + 1 // master key + 1 // account key + 2 // ext/int parent keys + (chain.getLookaheadSize() + chain.getLookaheadThreshold()) * 2 // lookahead zone on each chain ; assertEquals(numItems, keys.size()); // Get another key that will be lost during round-tripping, to ensure we can derive it again. DeterministicKey key4 = chain.getKey(KeyChain.KeyPurpose.CHANGE); final String EXPECTED_SERIALIZATION = checkSerialization(keys, "deterministic-wallet-serialization.txt"); // Round trip the data back and forth to check it is preserved. int oldLookaheadSize = chain.getLookaheadSize(); chain = DeterministicKeyChain.fromProtobuf(keys, null).get(0); assertEquals(EXPECTED_SERIALIZATION, protoToString(chain.serializeToProtobuf())); assertEquals(key1, chain.findKeyFromPubHash(key1.getPubKeyHash())); assertEquals(key2, chain.findKeyFromPubHash(key2.getPubKeyHash())); assertEquals(key3, chain.findKeyFromPubHash(key3.getPubKeyHash())); assertEquals(key4, chain.getKey(KeyChain.KeyPurpose.CHANGE)); key1.sign(Sha256Hash.ZERO_HASH); key2.sign(Sha256Hash.ZERO_HASH); key3.sign(Sha256Hash.ZERO_HASH); key4.sign(Sha256Hash.ZERO_HASH); assertEquals(oldLookaheadSize, chain.getLookaheadSize()); } @Test(expected = IllegalStateException.class) public void notEncrypted() { chain.toDecrypted("fail"); } @Test(expected = IllegalStateException.class) public void encryptTwice() { chain = chain.toEncrypted("once"); chain = chain.toEncrypted("twice"); } private void checkEncryptedKeyChain(DeterministicKeyChain encChain, DeterministicKey key1) { // Check we can look keys up and extend the chain without the AES key being provided. DeterministicKey encKey1 = encChain.findKeyFromPubKey(key1.getPubKey()); DeterministicKey encKey2 = encChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertFalse(key1.isEncrypted()); assertTrue(encKey1.isEncrypted()); assertEquals(encKey1.getPubKeyPoint(), key1.getPubKeyPoint()); final KeyParameter aesKey = checkNotNull(encChain.getKeyCrypter()).deriveKey("open secret"); encKey1.sign(Sha256Hash.ZERO_HASH, aesKey); encKey2.sign(Sha256Hash.ZERO_HASH, aesKey); assertTrue(encChain.checkAESKey(aesKey)); assertFalse(encChain.checkPassword("access denied")); assertTrue(encChain.checkPassword("open secret")); } @Test public void encryption() throws UnreadableWalletException { DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKeyChain encChain = chain.toEncrypted("open secret"); DeterministicKey encKey1 = encChain.findKeyFromPubKey(key1.getPubKey()); checkEncryptedKeyChain(encChain, key1); // Round-trip to ensure de/serialization works and that we can store two chains and they both deserialize. List<Protos.Key> serialized = encChain.serializeToProtobuf(); List<Protos.Key> doubled = Lists.newArrayListWithExpectedSize(serialized.size() * 2); doubled.addAll(serialized); doubled.addAll(serialized); final List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(doubled, encChain.getKeyCrypter()); assertEquals(2, chains.size()); encChain = chains.get(0); checkEncryptedKeyChain(encChain, chain.findKeyFromPubKey(key1.getPubKey())); encChain = chains.get(1); checkEncryptedKeyChain(encChain, chain.findKeyFromPubKey(key1.getPubKey())); DeterministicKey encKey2 = encChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); // Decrypt and check the keys match. DeterministicKeyChain decChain = encChain.toDecrypted("open secret"); DeterministicKey decKey1 = decChain.findKeyFromPubHash(encKey1.getPubKeyHash()); DeterministicKey decKey2 = decChain.findKeyFromPubHash(encKey2.getPubKeyHash()); assertEquals(decKey1.getPubKeyPoint(), encKey1.getPubKeyPoint()); assertEquals(decKey2.getPubKeyPoint(), encKey2.getPubKeyPoint()); assertFalse(decKey1.isEncrypted()); assertFalse(decKey2.isEncrypted()); assertNotEquals(encKey1.getParent(), decKey1.getParent()); // parts of a different hierarchy // Check we can once again derive keys from the decrypted chain. decChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).sign(Sha256Hash.ZERO_HASH); decChain.getKey(KeyChain.KeyPurpose.CHANGE).sign(Sha256Hash.ZERO_HASH); } @Test public void watchingChain() throws UnreadableWalletException { Utils.setMockClock(); DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key3 = chain.getKey(KeyChain.KeyPurpose.CHANGE); DeterministicKey key4 = chain.getKey(KeyChain.KeyPurpose.CHANGE); NetworkParameters params = MainNetParams.get(); DeterministicKey watchingKey = chain.getWatchingKey(); final String pub58 = watchingKey.serializePubB58(params); assertEquals("xpub69KR9epSNBM59KLuasxMU5CyKytMJjBP5HEZ5p8YoGUCpM6cM9hqxB9DDPCpUUtqmw5duTckvPfwpoWGQUFPmRLpxs5jYiTf2u6xRMcdhDf", pub58); watchingKey = DeterministicKey.deserializeB58(null, pub58, params); watchingKey.setCreationTimeSeconds(100000); chain = DeterministicKeyChain.watch(watchingKey); assertEquals(100000, chain.getEarliestKeyCreationTime()); chain.setLookaheadSize(10); chain.maybeLookAhead(); assertEquals(key1.getPubKeyPoint(), chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint()); assertEquals(key2.getPubKeyPoint(), chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint()); final DeterministicKey key = chain.getKey(KeyChain.KeyPurpose.CHANGE); assertEquals(key3.getPubKeyPoint(), key.getPubKeyPoint()); try { // Can't sign with a key from a watching chain. key.sign(Sha256Hash.ZERO_HASH); fail(); } catch (ECKey.MissingPrivateKeyException e) { // Ignored. } // Test we can serialize and deserialize a watching chain OK. List<Protos.Key> serialization = chain.serializeToProtobuf(); checkSerialization(serialization, "watching-wallet-serialization.txt"); chain = DeterministicKeyChain.fromProtobuf(serialization, null).get(0); final DeterministicKey rekey4 = chain.getKey(KeyChain.KeyPurpose.CHANGE); assertEquals(key4.getPubKeyPoint(), rekey4.getPubKeyPoint()); } @Test(expected = IllegalStateException.class) public void watchingCannotEncrypt() throws Exception { final DeterministicKey accountKey = chain.getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH); chain = DeterministicKeyChain.watch(accountKey.dropPrivateBytes().dropParent()); chain = chain.toEncrypted("this doesn't make any sense"); } @Test public void bloom1() { DeterministicKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); int numEntries = (((chain.getLookaheadSize() + chain.getLookaheadThreshold()) * 2) // * 2 because of internal/external + chain.numLeafKeysIssued() + 4 // one root key + one account key + two chain keys (internal/external) ) * 2; // because the filter contains keys and key hashes. assertEquals(numEntries, chain.numBloomFilterEntries()); BloomFilter filter = chain.getFilter(numEntries, 0.001, 1); assertTrue(filter.contains(key1.getPubKey())); assertTrue(filter.contains(key1.getPubKeyHash())); assertTrue(filter.contains(key2.getPubKey())); assertTrue(filter.contains(key2.getPubKeyHash())); // The lookahead zone is tested in bloom2 and via KeyChainGroupTest.bloom } @Test public void bloom2() throws Exception { // Verify that if when we watch a key, the filter contains at least 100 keys. DeterministicKey[] keys = new DeterministicKey[100]; for (int i = 0; i < keys.length; i++) keys[i] = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); chain = DeterministicKeyChain.watch(chain.getWatchingKey().dropPrivateBytes().dropParent()); int e = chain.numBloomFilterEntries(); BloomFilter filter = chain.getFilter(e, 0.001, 1); for (DeterministicKey key : keys) assertTrue("key " + key, filter.contains(key.getPubKeyHash())); } private String protoToString(List<Protos.Key> keys) { StringBuilder sb = new StringBuilder(); for (Protos.Key key : keys) { sb.append(key.toString()); sb.append("\n"); } return sb.toString().trim(); } private String checkSerialization(List<Protos.Key> keys, String filename) { try { String sb = protoToString(keys); String expected = Utils.getResourceAsString(getClass().getResource(filename)); assertEquals(expected, sb); return expected; } catch (IOException e) { throw new RuntimeException(e); } } }