KeyChainGroup: remove upgrade path from basic to deterministic

This commit is contained in:
Andreas Schildbach 2022-03-24 12:47:44 +01:00
parent 20aee84773
commit 4e8a19997d
3 changed files with 5 additions and 250 deletions

View file

@ -218,7 +218,7 @@ public class KeyChainGroup implements KeyBag {
private BasicKeyChain basic;
private final NetworkParameters params;
// Keychains for deterministically derived keys. If this is null, no chains should be created automatically.
// Keychains for deterministically derived keys.
protected final @Nullable LinkedList<DeterministicKeyChain> chains;
// currentKeys is used for normal, non-multisig/married wallets. currentAddresses is used when we're handing out
// P2SH addresses. They're mutually exclusive.
@ -282,7 +282,7 @@ public class KeyChainGroup implements KeyBag {
}
}
/** Returns true if it contains any deterministic keychain or one could be created. */
/** Returns true if it contains any deterministic keychain. */
public boolean isSupportsDeterministicChains() {
return chains != null;
}
@ -949,62 +949,13 @@ public class KeyChainGroup implements KeyBag {
*/
public void upgradeToDeterministic(Script.ScriptType preferredScriptType, KeyChainGroupStructure structure,
long keyRotationTimeSecs, @Nullable KeyParameter aesKey)
throws DeterministicUpgradeRequiresPassword, AllRandomKeysRotating {
throws DeterministicUpgradeRequiresPassword {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
checkNotNull(structure);
checkArgument(keyRotationTimeSecs >= 0);
if (!isDeterministicUpgradeRequired(preferredScriptType, keyRotationTimeSecs))
return; // Nothing to do.
// Basic --> P2PKH upgrade
if (basic.numKeys() > 0 && getActiveKeyChain(Script.ScriptType.P2PKH, keyRotationTimeSecs) == null) {
// Subtract one because the key rotation time might have been set to the creation time of the first known good
// key, in which case, that's the one we want to find.
ECKey keyToUse = basic.findOldestKeyAfter(keyRotationTimeSecs - 1);
if (keyToUse == null)
throw new AllRandomKeysRotating();
boolean keyWasEncrypted = keyToUse.isEncrypted();
if (keyWasEncrypted) {
if (aesKey == null) {
// We can't auto upgrade because we don't know the users password at this point. We throw an
// exception so the calling code knows to abort the load and ask the user for their password, they can
// then try loading the wallet again passing in the AES key.
//
// There are a few different approaches we could have used here, but they all suck. The most obvious
// is to try and be as lazy as possible, running in the old random-wallet mode until the user enters
// their password for some other reason and doing the upgrade then. But this could result in strange
// and unexpected UI flows for the user, as well as complicating the job of wallet developers who then
// have to support both "old" and "new" UI modes simultaneously, switching them on the fly. Given that
// this is a one-off transition, it seems more reasonable to just ask the user for their password
// on startup, and then the wallet app can have all the widgets for accessing seed words etc active
// all the time.
throw new DeterministicUpgradeRequiresPassword();
}
keyToUse = keyToUse.decrypt(aesKey);
} else if (aesKey != null) {
throw new IllegalStateException("AES Key was provided but wallet is not encrypted.");
}
log.info(
"Upgrading from basic keychain to P2PKH deterministic keychain. Using oldest non-rotating private key (address: {})",
LegacyAddress.fromKey(params, keyToUse));
byte[] entropy = checkNotNull(keyToUse.getSecretBytes());
// Private keys should be at least 128 bits long.
checkState(entropy.length >= DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8);
// We reduce the entropy here to 128 bits because people like to write their seeds down on paper, and 128
// bits should be sufficient forever unless the laws of the universe change or ECC is broken; in either case
// we all have bigger problems.
entropy = Arrays.copyOfRange(entropy, 0, DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8); // final argument is exclusive range.
checkState(entropy.length == DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS / 8);
DeterministicKeyChain chain = DeterministicKeyChain.builder()
.entropy(entropy, keyToUse.getCreationTimeSeconds())
.outputScriptType(Script.ScriptType.P2PKH)
.accountPath(structure.accountPathFor(Script.ScriptType.P2PKH)).build();
if (keyWasEncrypted)
chain = chain.toEncrypted(checkNotNull(keyCrypter), aesKey);
addAndActivateHDChain(chain);
}
// P2PKH --> P2WPKH upgrade
if (preferredScriptType == Script.ScriptType.P2WPKH
&& getActiveKeyChain(Script.ScriptType.P2WPKH, keyRotationTimeSecs) == null) {

View file

@ -542,13 +542,7 @@ public class KeyChainGroupTest {
@Test
public void deterministicUpgradeUnencrypted() throws Exception {
// Check that a group that contains only random keys has its HD chain created using the private key bytes of
// the oldest random key, so upgrading the same wallet twice gives the same outcome.
group = KeyChainGroup.builder(MAINNET).lookaheadSize(LOOKAHEAD_SIZE).build();
ECKey key1 = new ECKey();
Utils.rollMockClock(86400);
ECKey key2 = new ECKey();
group.importKeys(key2, key1);
group = KeyChainGroup.builder(MAINNET).fromRandom(Script.ScriptType.P2PKH).lookaheadSize(LOOKAHEAD_SIZE).build();
List<Protos.Key> protobufs = group.serializeToProtobuf();
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, null);
@ -568,45 +562,12 @@ public class KeyChainGroupTest {
DeterministicSeed seed2 = group.getActiveKeyChain().getSeed();
assertEquals(seed1, seed2);
assertEquals(dkey1, dkey2);
// Check we used the right (oldest) key despite backwards import order.
byte[] truncatedBytes = Arrays.copyOfRange(key1.getSecretBytes(), 0, 16);
assertArrayEquals(seed1.getEntropyBytes(), truncatedBytes);
}
@Test
public void deterministicUpgradeRotating() throws Exception {
group = KeyChainGroup.builder(MAINNET).lookaheadSize(LOOKAHEAD_SIZE).build();
long now = Utils.currentTimeSeconds();
ECKey key1 = new ECKey();
Utils.rollMockClock(86400);
ECKey key2 = new ECKey();
Utils.rollMockClock(86400);
ECKey key3 = new ECKey();
group.importKeys(key2, key1, key3);
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, now + 10, null);
DeterministicSeed seed = group.getActiveKeyChain().getSeed();
assertNotNull(seed);
// Check we used the right key: oldest non rotating.
byte[] truncatedBytes = Arrays.copyOfRange(key2.getSecretBytes(), 0, 16);
assertArrayEquals(seed.getEntropyBytes(), truncatedBytes);
}
@Test
public void deterministicUpgradeEncrypted() throws Exception {
group = KeyChainGroup.builder(MAINNET).build();
final ECKey key = new ECKey();
group.importKeys(key);
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
group = KeyChainGroup.builder(MAINNET).fromRandom(Script.ScriptType.P2PKH).build();
group.encrypt(KEY_CRYPTER, AES_KEY);
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
try {
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, null);
fail();
} catch (DeterministicUpgradeRequiresPassword e) {
// Expected.
}
group.upgradeToDeterministic(Script.ScriptType.P2PKH, KeyChainGroupStructure.DEFAULT, 0, AES_KEY);
assertTrue(group.isEncrypted());
assertFalse(group.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH, 0));
assertTrue(group.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH, 0));
@ -614,9 +575,6 @@ public class KeyChainGroupTest {
assertNotNull(deterministicSeed);
assertTrue(deterministicSeed.isEncrypted());
byte[] entropy = checkNotNull(group.getActiveKeyChain().toDecrypted(AES_KEY).getSeed()).getEntropyBytes();
// Check we used the right key: oldest non rotating.
byte[] truncatedBytes = Arrays.copyOfRange(key.getSecretBytes(), 0, 16);
assertArrayEquals(entropy, truncatedBytes);
}
@Test

View file

@ -2982,55 +2982,6 @@ public class WalletTest extends TestWithWallet {
assertNotEquals(watchKey1, watchKey2);
}
@SuppressWarnings("ConstantConditions")
@Test
public void keyRotationHD2() throws Exception {
// Check we handle the following scenario: a weak random key is created, then some good random keys are created
// but the weakness of the first isn't known yet. The wallet is upgraded to HD based on the weak key. Later, we
// find out about the weakness and set the rotation time to after the bad key's creation date. A new HD chain
// should be created based on the oldest known good key and the old chain + bad random key should rotate to it.
// We fix the private keys just to make the test deterministic (last byte differs).
Utils.setMockClock();
ECKey badKey = ECKey.fromPrivate(Utils.HEX.decode("00905b93f990267f4104f316261fc10f9f983551f9ef160854f40102eb71cffdbb"));
badKey.setCreationTimeSeconds(Utils.currentTimeSeconds());
Utils.rollMockClock(86400);
ECKey goodKey = ECKey.fromPrivate(Utils.HEX.decode("00905b93f990267f4104f316261fc10f9f983551f9ef160854f40102eb71cffdcc"));
goodKey.setCreationTimeSeconds(Utils.currentTimeSeconds());
// Do an upgrade based on the bad key.
KeyChainGroup kcg = KeyChainGroup.builder(UNITTEST).build();
kcg.importKeys(badKey, goodKey);
Utils.rollMockClock(86400);
wallet = new Wallet(UNITTEST, kcg); // This avoids the automatic HD initialisation
assertTrue(kcg.getDeterministicKeyChains().isEmpty());
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
DeterministicKey badWatchingKey = wallet.getWatchingKey();
assertEquals(badKey.getCreationTimeSeconds(), badWatchingKey.getCreationTimeSeconds());
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, LegacyAddress.fromKey(UNITTEST, badWatchingKey));
// Now we set the rotation time to the time we started making good keys. This should create a new HD chain.
wallet.setKeyRotationTime(goodKey.getCreationTimeSeconds());
List<Transaction> txns = wallet.doMaintenance(null, false).get();
assertEquals(1, txns.size());
Address output = txns.get(0).getOutput(0).getScriptPubKey().getToAddress(UNITTEST);
ECKey usedKey = wallet.findKeyFromPubKeyHash(output.getHash(), output.getOutputScriptType());
assertEquals(goodKey.getCreationTimeSeconds(), usedKey.getCreationTimeSeconds());
assertEquals(goodKey.getCreationTimeSeconds(), wallet.freshReceiveKey().getCreationTimeSeconds());
assertEquals("mrM3TpCnav5YQuVA1xLercCGJH4DXujMtv", LegacyAddress.fromKey(UNITTEST, usedKey).toString());
DeterministicKeyChain c = kcg.getDeterministicKeyChains().get(1);
assertEquals(c.getEarliestKeyCreationTime(), goodKey.getCreationTimeSeconds());
assertEquals(2, kcg.getDeterministicKeyChains().size());
// Commit the maint txns.
wallet.commitTx(txns.get(0));
// Check next maintenance does nothing.
assertTrue(wallet.doMaintenance(null, false).get().isEmpty());
assertEquals(c, kcg.getDeterministicKeyChains().get(1));
assertEquals(2, kcg.getDeterministicKeyChains().size());
}
@Test(expected = IllegalArgumentException.class)
public void importOfHDKeyForbidden() throws Exception {
wallet.importKey(wallet.freshReceiveKey());
@ -3234,108 +3185,6 @@ public class WalletTest extends TestWithWallet {
assertEquals(1, keys.size());
}
@Test
public void upgradeToDeterministic_basic_to_P2PKH_unencrypted() throws Exception {
wallet = new Wallet(UNITTEST, KeyChainGroup.builder(UNITTEST).build());
wallet.importKeys(Arrays.asList(new ECKey(), new ECKey()));
assertFalse(wallet.isEncrypted());
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
try {
wallet.freshReceiveKey();
fail();
} catch (DeterministicUpgradeRequiredException e) {
// Expected.
}
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
assertFalse(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2PKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2PKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_basic_to_P2PKH_encrypted() throws Exception {
wallet = new Wallet(UNITTEST, KeyChainGroup.builder(UNITTEST).build());
wallet.importKeys(Arrays.asList(new ECKey(), new ECKey()));
assertFalse(wallet.isEncrypted());
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
KeyParameter aesKey = new KeyCrypterScrypt(SCRYPT_ITERATIONS).deriveKey("abc");
wallet.encrypt(new KeyCrypterScrypt(), aesKey);
assertTrue(wallet.isEncrypted());
try {
wallet.freshReceiveKey();
fail();
} catch (DeterministicUpgradeRequiredException e) {
// Expected.
}
try {
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
fail();
} catch (DeterministicUpgradeRequiresPassword e) {
// Expected.
}
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, aesKey);
assertTrue(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2PKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2PKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_basic_to_P2WPKH_unencrypted() throws Exception {
wallet = new Wallet(UNITTEST, KeyChainGroup.builder(UNITTEST).build());
wallet.importKeys(Arrays.asList(new ECKey(), new ECKey()));
assertFalse(wallet.isEncrypted());
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
try {
wallet.freshReceiveKey();
fail();
} catch (DeterministicUpgradeRequiredException e) {
// Expected.
}
wallet.upgradeToDeterministic(Script.ScriptType.P2WPKH, null);
assertFalse(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2WPKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2WPKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_basic_to_P2WPKH_encrypted() throws Exception {
wallet = new Wallet(UNITTEST, KeyChainGroup.builder(UNITTEST).build());
wallet.importKeys(Arrays.asList(new ECKey(), new ECKey()));
assertFalse(wallet.isEncrypted());
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertTrue(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
KeyParameter aesKey = new KeyCrypterScrypt(SCRYPT_ITERATIONS).deriveKey("abc");
wallet.encrypt(new KeyCrypterScrypt(), aesKey);
assertTrue(wallet.isEncrypted());
try {
wallet.upgradeToDeterministic(Script.ScriptType.P2WPKH, null);
fail();
} catch (DeterministicUpgradeRequiresPassword e) {
// Expected.
}
wallet.upgradeToDeterministic(Script.ScriptType.P2WPKH, aesKey);
assertTrue(wallet.isEncrypted());
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2PKH));
assertFalse(wallet.isDeterministicUpgradeRequired(Script.ScriptType.P2WPKH));
assertEquals(Script.ScriptType.P2WPKH, wallet.currentReceiveAddress().getOutputScriptType());
assertEquals(Script.ScriptType.P2WPKH, wallet.freshReceiveAddress().getOutputScriptType());
}
@Test
public void upgradeToDeterministic_P2PKH_to_P2WPKH_unencrypted() throws Exception {
wallet = Wallet.createDeterministic(UNITTEST, Script.ScriptType.P2PKH);
@ -3489,9 +3338,6 @@ public class WalletTest extends TestWithWallet {
Wallet wallet = Wallet.fromKeys(UNITTEST, Arrays.asList(key));
assertEquals(1, wallet.getImportedKeys().size());
assertEquals(key, wallet.getImportedKeys().get(0));
wallet.upgradeToDeterministic(Script.ScriptType.P2PKH, null);
String seed = wallet.getKeyChainSeed().toHexString();
assertEquals("5ca8cd6c01aa004d3c5396c628b78a4a89462f412f460a845b594ac42eceaa264b0e14dcd4fe73d4ed08ce06f4c28facfa85042d26d784ab2798a870bb7af556", seed);
}
@Test