add file import of the xpub descriptor jade writes to usb storage
What changed, and why it matters
This commit adds the ability to import a Bitcoin wallet's extended public key (xpub) into Sparrow Wallet from a file written by a Blockstream Jade hardware wallet via USB storage. Previously, Jade only supported QR-code import. The change parses a descriptor string from a file, validates it matches the expected script type, and creates a keystore. There is no direct evidence in the commit or supplied references that this is a security fix or vulnerability; it appears to be a normal feature addition.
Review the drongo submodule bump (340e23fcb037b7839616001fff0b44ce4e51e800) to confirm OutputDescriptor parsing correctly validates checksums, derivation paths, and key origin information. Ensure the new file import path cannot be abused by malformed descriptors or path confusion, and consider adding tests for invalid descriptors and multi-keystore descriptors.
Security signals we found
New file import path parses external descriptor data and converts it to a keystore
Script type mismatch is explicitly rejected with an IllegalArgumentException
Silent payments policy (SINGLE_SP) is explicitly rejected
No input sanitization beyond descriptor parsing and script-type check is visible
No vendor disclosure of security relevance in commit or supplied references
Evidence from the diff
The commit modifies Jade.java to implement file-based keystore import. It reads UTF-8 text from an InputStream, parses it as an OutputDescriptor, validates the script type matches the requested ScriptType, converts the descriptor to a Wallet, extracts the single Keystore, and sets its label, wallet model, and source to HW_AIRGAPPED. It also enables isFileFormatAvailable() and updates the import description. A drongo submodule bump is included, likely to add OutputDescriptor support. Tests verify successful P2WPKH import, script-type mismatch rejection, and silent-payments policy rejection.
Changed components
src/main/java/com/sparrowwallet/sparrow/io/Jade.javadrongo submoduleJade hardware wallet integration / keystore import flowInspect captured patch +74 / −4
### drongo
@@ -1 +1 @@
-Subproject commit a19eb5d9d4322469e4e07cd19da3e182cbd56639
+Subproject commit 340e23fcb037b7839616001fff0b44ce4e51e800
### src/main/java/com/sparrowwallet/sparrow/io/Jade.java
@@ -1,12 +1,18 @@
package com.sparrowwallet.sparrow.io;
+import com.google.common.io.CharStreams;
+import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
+import com.sparrowwallet.drongo.wallet.KeystoreSource;
+import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.File;
import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
public class Jade implements KeystoreFileImport {
@Override
@@ -26,12 +32,35 @@ public WalletModel getWalletModel() {
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
- throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
+ try {
+ String text = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).trim();
+ OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(text);
+ if(policyType == PolicyType.SINGLE_SP) {
+ throw new IllegalArgumentException("Export does not contain the spscan value for silent payments");
+ } else if(outputDescriptor.getScriptType() != scriptType) {
+ throw new IllegalArgumentException("The exported xpub is for " + outputDescriptor.getScriptType().getDescription() + ", not " + scriptType.getDescription()
+ + ". Select " + scriptType.getDescription() + " in the options when using Export Xpub on your Jade, and export again.");
+ }
+
+ Wallet wallet = outputDescriptor.toWallet();
+ if(wallet.getKeystores().size() != 1) {
+ throw new IllegalArgumentException("Could not determine keystore from import");
+ }
+
+ Keystore keystore = wallet.getKeystores().getFirst();
+ keystore.setLabel(getName());
+ keystore.setWalletModel(getWalletModel());
+ keystore.setSource(KeystoreSource.HW_AIRGAPPED);
+
+ return keystore;
+ } catch(Exception e) {
+ throw new ImportException("Error getting " + getName() + " keystore", e);
+ }
}
@Override
public String getKeystoreImportDescription(int account) {
- return "Import QR created on your Jade by selecting Options > Wallet > Export Xpub once you have loaded your seed. Make sure to select Singlesig as the Wallet type in the Options menu there.";
+ return "Import QR created on your Jade by selecting Options > Wallet > Export Xpub once you have loaded your seed, or the file written by Options > USB Storage > Export Xpub. Make sure to select Singlesig as the Wallet type in the Options menu there.";
}
@Override
@@ -41,6 +70,6 @@ public boolean isKeystoreImportScannable() {
@Override
public boolean isFileFormatAvailable() {
- return false;
+ return true;
}
}
### src/test/java/com/sparrowwallet/sparrow/io/JadeTest.java
@@ -0,0 +1,40 @@
+package com.sparrowwallet.sparrow.io;
+
+import com.sparrowwallet.drongo.ExtendedKey;
+import com.sparrowwallet.drongo.policy.PolicyType;
+import com.sparrowwallet.drongo.protocol.ScriptType;
+import com.sparrowwallet.drongo.wallet.Keystore;
+import com.sparrowwallet.drongo.wallet.KeystoreSource;
+import com.sparrowwallet.drongo.wallet.WalletModel;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class JadeTest extends IoTest {
+ @Test
+ public void testImport() throws ImportException {
+ Jade jade = new Jade();
+ Keystore keystore = jade.getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, getInputStream("jade-keystore.txt"), null);
+
+ Assertions.assertEquals("Jade", keystore.getLabel());
+ Assertions.assertEquals(WalletModel.JADE, keystore.getWalletModel());
+ Assertions.assertEquals(KeystoreSource.HW_AIRGAPPED, keystore.getSource());
+ Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
+ Assertions.assertEquals("73c5da0a", keystore.getKeyDerivation().getMasterFingerprint());
+ Assertions.assertEquals(ExtendedKey.fromDescriptor("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs"), keystore.getExtendedPublicKey());
+ Assertions.assertTrue(keystore.isValid());
+ }
+
+ @Test
+ public void testImportScriptTypeMismatch() {
+ Jade jade = new Jade();
+ ImportException scriptTypeException = Assertions.assertThrows(ImportException.class, () -> jade.getKeystore(PolicyType.SINGLE_HD, ScriptType.P2TR, getInputStream("jade-keystore.txt"), null));
+ Assertions.assertTrue(scriptTypeException.getCause().getMessage().startsWith("The exported xpub is for Native Segwit (P2WPKH), not Taproot (P2TR)."));
+ }
+
+ @Test
+ public void testImportSilentPayments() {
+ Jade jade = new Jade();
+ ImportException silentPaymentsException = Assertions.assertThrows(ImportException.class, () -> jade.getKeystore(PolicyType.SINGLE_SP, ScriptType.P2TR, getInputStream("jade-keystore.txt"), null));
+ Assertions.assertEquals("Export does not contain the spscan value for silent payments", silentPaymentsException.getCause().getMessage());
+ }
+}
### src/test/resources/com/sparrowwallet/sparrow/io/jade-keystore.txt
@@ -0,0 +1 @@
+wpkh([73c5da0a/84'/0'/0']xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V/0/*)#wc3n3van
\ No newline at end of fileWhy this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.