From 948d663fbf1814211e602df896ca6f32ef61ca86 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 17 May 2022 08:04:57 +0200 Subject: [PATCH] sign psbt from a transient scanned seed (seedqr, compactseedqr, ur:crypto-seed, ur:crypto-bip39 supported) --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 10 +++- .../sparrow/control/QRScanDialog.java | 60 ++++++++++++++++++- .../sparrow/event/ViewTransactionEvent.java | 15 +++++ .../transaction/HeadersController.java | 47 ++++++++++++++- .../transaction/TransactionController.java | 2 +- 6 files changed, 129 insertions(+), 7 deletions(-) diff --git a/drongo b/drongo index eddd6406..38deacae 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit eddd6406efc20b83e659d59faa16189417b0f5ed +Subproject commit 38deacaeec757acaaaedada7c17e9338564cb389 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index fff2ceac..e5d65abb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1732,6 +1732,10 @@ public class AppController implements Initializable { } } + private void addTransactionTab(Transaction transaction, TransactionView initialView, Integer initialIndex) { + addTransactionTab(null, null, transaction, null, null, initialView, initialIndex); + } + private void addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { addTransactionTab(null, null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex); } @@ -2588,7 +2592,11 @@ public class AppController implements Initializable { @Subscribe public void viewTransaction(ViewTransactionEvent event) { if(tabs.getScene().getWindow().equals(event.getWindow())) { - addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex()); + if(event.getBlockTransaction() != null) { + addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex()); + } else { + addTransactionTab(event.getTransaction(), event.getInitialView(), event.getInitialIndex()); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index f61a0820..881db17d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -10,15 +10,14 @@ import com.sparrowwallet.drongo.address.P2PKHAddress; import com.sparrowwallet.drongo.address.P2SHAddress; import com.sparrowwallet.drongo.address.P2WPKHAddress; import com.sparrowwallet.drongo.crypto.*; -import com.sparrowwallet.drongo.policy.Policy; -import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.Base43; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.uri.BitcoinURI; -import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.DeterministicSeed; +import com.sparrowwallet.drongo.wallet.SeedQR; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.hummingbird.LegacyURDecoder; import com.sparrowwallet.hummingbird.registry.*; @@ -257,6 +256,7 @@ public class QRScanDialog extends Dialog { BitcoinURI bitcoinURI; Address address; ExtendedKey extendedKey; + DeterministicSeed seed; try { extendedKey = ExtendedKey.fromDescriptor(qrtext); result = new Result(extendedKey); @@ -334,6 +334,22 @@ public class QRScanDialog extends Dialog { //Ignore, not parseable as base43 decoded bytes } + try { + seed = SeedQR.getSeed(qrtext); + result = new Result(seed); + return; + } catch(Exception e) { + //Ignore, not parseable as a SeedQR + } + + try { + seed = SeedQR.getSeed(qrResult.getRawBytes()); + result = new Result(seed); + return; + } catch(Exception e) { + //Ignore, not parseable as a CompactSeedQR + } + result = new Result(qrtext); } } @@ -401,6 +417,14 @@ public class QRScanDialog extends Dialog { CryptoAccount cryptoAccount = (CryptoAccount)ur.decodeFromRegistry(); List wallets = getWallets(cryptoAccount); return new Result(wallets); + } else if(urRegistryType.equals(RegistryType.CRYPTO_SEED)) { + CryptoSeed cryptoSeed = (CryptoSeed)ur.decodeFromRegistry(); + DeterministicSeed seed = getSeed(cryptoSeed); + return new Result(seed); + } else if(urRegistryType.equals(RegistryType.CRYPTO_BIP39)) { + CryptoBip39 cryptoBip39 = (CryptoBip39)ur.decodeFromRegistry(); + DeterministicSeed seed = getSeed(cryptoBip39); + return new Result(seed); } else { log.error("Unsupported UR type " + urRegistryType); return new Result(new URException("UR type " + urRegistryType + " is not supported")); @@ -523,6 +547,14 @@ public class QRScanDialog extends Dialog { return null; } + + private DeterministicSeed getSeed(CryptoSeed cryptoSeed) { + return new DeterministicSeed(cryptoSeed.getSeed(), null, cryptoSeed.getBirthdate().getTime()); + } + + private DeterministicSeed getSeed(CryptoBip39 cryptoBip39) { + return new DeterministicSeed(cryptoBip39.getWords(), null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39); + } } private class QRScanListener implements WebcamListener { @@ -626,6 +658,7 @@ public class QRScanDialog extends Dialog { public final ExtendedKey extendedKey; public final OutputDescriptor outputDescriptor; public final List wallets; + public final DeterministicSeed seed; public final String payload; public final Throwable exception; @@ -636,6 +669,7 @@ public class QRScanDialog extends Dialog { this.extendedKey = null; this.outputDescriptor = null; this.wallets = null; + this.seed = null; this.payload = null; this.exception = null; } @@ -647,6 +681,7 @@ public class QRScanDialog extends Dialog { this.extendedKey = null; this.outputDescriptor = null; this.wallets = null; + this.seed = null; this.payload = null; this.exception = null; } @@ -658,6 +693,7 @@ public class QRScanDialog extends Dialog { this.extendedKey = null; this.outputDescriptor = null; this.wallets = null; + this.seed = null; this.payload = null; this.exception = null; } @@ -669,6 +705,7 @@ public class QRScanDialog extends Dialog { this.extendedKey = null; this.outputDescriptor = null; this.wallets = null; + this.seed = null; this.payload = null; this.exception = null; } @@ -680,6 +717,7 @@ public class QRScanDialog extends Dialog { this.extendedKey = extendedKey; this.outputDescriptor = null; this.wallets = null; + this.seed = null; this.payload = null; this.exception = null; } @@ -691,6 +729,7 @@ public class QRScanDialog extends Dialog { this.extendedKey = null; this.outputDescriptor = outputDescriptor; this.wallets = null; + this.seed = null; this.payload = null; this.exception = null; } @@ -702,6 +741,19 @@ public class QRScanDialog extends Dialog { this.extendedKey = null; this.outputDescriptor = null; this.wallets = wallets; + this.seed = null; + this.payload = null; + this.exception = null; + } + + public Result(DeterministicSeed seed) { + this.transaction = null; + this.psbt = null; + this.uri = null; + this.extendedKey = null; + this.outputDescriptor = null; + this.wallets = null; + this.seed = seed; this.payload = null; this.exception = null; } @@ -713,6 +765,7 @@ public class QRScanDialog extends Dialog { this.extendedKey = null; this.outputDescriptor = null; this.wallets = null; + this.seed = null; this.payload = payload; this.exception = null; } @@ -724,6 +777,7 @@ public class QRScanDialog extends Dialog { this.extendedKey = null; this.outputDescriptor = null; this.wallets = null; + this.seed = null; this.payload = null; this.exception = exception; } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ViewTransactionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ViewTransactionEvent.java index 17812473..4e749cea 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/ViewTransactionEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/ViewTransactionEvent.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.event; +import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.sparrow.transaction.TransactionView; import com.sparrowwallet.sparrow.wallet.HashIndexEntry; @@ -7,10 +8,19 @@ import javafx.stage.Window; public class ViewTransactionEvent { private final Window window; + private final Transaction transaction; private final BlockTransaction blockTransaction; private final TransactionView initialView; private final Integer initialIndex; + public ViewTransactionEvent(Window window, Transaction transaction) { + this.window = window; + this.transaction = transaction; + this.blockTransaction = null; + this.initialView = TransactionView.HEADERS; + this.initialIndex = null; + } + public ViewTransactionEvent(Window window, BlockTransaction blockTransaction) { this(window, blockTransaction, TransactionView.HEADERS, null); } @@ -21,6 +31,7 @@ public class ViewTransactionEvent { public ViewTransactionEvent(Window window, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { this.window = window; + this.transaction = blockTransaction.getTransaction(); this.blockTransaction = blockTransaction; this.initialView = initialView; this.initialIndex = initialIndex; @@ -30,6 +41,10 @@ public class ViewTransactionEvent { return window; } + public Transaction getTransaction() { + return transaction; + } + public BlockTransaction getBlockTransaction() { return blockTransaction; } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index c5149979..b2479a75 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -60,6 +60,9 @@ import java.time.*; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; public class HeadersController extends TransactionFormController implements Initializable, DynamicUpdate { private static final Logger log = LoggerFactory.getLogger(HeadersController.class); @@ -829,7 +832,49 @@ public class HeadersController extends TransactionFormController implements Init ToggleButton toggleButton = (ToggleButton)event.getSource(); toggleButton.setSelected(false); - EventManager.get().post(new RequestQRScanEvent(toggleButton.getScene().getWindow())); + QRScanDialog qrScanDialog = new QRScanDialog(); + Optional optionalResult = qrScanDialog.showAndWait(); + if(optionalResult.isPresent()) { + QRScanDialog.Result result = optionalResult.get(); + if(result.transaction != null) { + EventManager.get().post(new ViewTransactionEvent(toggleButton.getScene().getWindow(), result.transaction)); + } else if(result.psbt != null) { + EventManager.get().post(new ViewPSBTEvent(toggleButton.getScene().getWindow(), null, null, result.psbt)); + } else if(result.seed != null) { + signFromSeed(result.seed); + } else if(result.exception != null) { + log.error("Error scanning QR", result.exception); + showErrorDialog("Error scanning QR", result.exception.getMessage()); + } else { + AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a transaction or seed."); + } + } + } + + private void signFromSeed(DeterministicSeed seed) { + try { + String masterFingerprint = Keystore.fromSeed(seed, ScriptType.P2PKH.getDefaultDerivation()).getKeyDerivation().getMasterFingerprint(); + Wallet walletCopy = headersForm.getSigningWallet().copy(); + OptionalInt optIndex = IntStream.range(0, walletCopy.getKeystores().size()) + .filter(i -> walletCopy.getKeystores().get(i).getKeyDerivation().getMasterFingerprint().equals(masterFingerprint)).findFirst(); + if(optIndex.isPresent()) { + walletCopy.getKeystores().forEach(keystore -> { + keystore.setSeed(null); + keystore.setMasterPrivateExtendedKey(null); + }); + Keystore original = walletCopy.getKeystores().get(optIndex.getAsInt()); + Keystore replacement = Keystore.fromSeed(seed, original.getKeyDerivation().getDerivation()); + walletCopy.getKeystores().set(optIndex.getAsInt(), replacement); + signUnencryptedKeystores(walletCopy); + } else { + AppServices.showErrorDialog("Invalid seed", "The QR code contains a seed that does not match any of the keystores in the signing wallet."); + } + } catch(MnemonicException e) { + log.error("Invalid seed", e); + AppServices.showErrorDialog("Invalid seed", e.getMessage()); + } finally { + seed.clear(); + } } public void savePSBT(ActionEvent event) { diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java index 823a3723..e2825c5f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java @@ -432,7 +432,7 @@ public class TransactionController implements Initializable { @Subscribe public void viewTransaction(ViewTransactionEvent event) { - if(txdata.getTransaction().getTxId().equals(event.getBlockTransaction().getTransaction().getTxId())) { + if(txdata.getTransaction().getTxId().equals(event.getTransaction().getTxId())) { TreeItem existingItem = getTreeItem(event.getInitialView(), event.getInitialIndex()); if(existingItem != null && !(existingItem.getValue() instanceof PageForm)) { setTreeSelection(event.getInitialView(), event.getInitialIndex());