support retrieving silent payments spscan keys via connected devices
What changed, and why it matters
This commit adds support for importing a special Bitcoin 'silent payments' scanning key from connected hardware wallets, while explicitly blocking three types of smartcards (Tapsigner, Keycard, Satochip) from being used with silent-payment wallets because they cannot support it. It is a feature addition, not a fix for an active security flaw.
No immediate security action required. Treat as a normal feature commit. If reviewing further, verify that Lark.getSpscanAtPath() correctly validates the derivation path and that the returned SilentPaymentScanAddress is handled safely in downstream wallet code.
Security signals we found
New feature: silent payments scan-key import from hardware devices
Explicit guard added to reject unsupported card imports for silent-payment wallets
No input sanitization change beyond existing derivation-path handling
No cryptographic operations implemented in the changed code; calls out to existing Lark/HWI layer
Evidence from the diff
The patch introduces a new code path in Sparrow Wallet for retrieving a SilentPaymentScanAddress (spscan) from USB hardware devices via HWI/Lark when the wallet policy type is SINGLE_SP. It adds Hwi.getSpscan(), a JavaFX Service wrapper, and DevicePane.importSpscan(). For card-based keystore imports (Tapsigner, Keycard, Satochip), it adds an upfront guard that throws ImportException if the policy type is SINGLE_SP. No vulnerability, buffer overflow, injection, or key-leakage pattern is visible in the diff.
Changed components
com.sparrowwallet.sparrow.control.DevicePanecom.sparrowwallet.sparrow.io.Hwicom.sparrowwallet.sparrow.io.ckcard.Tapsignercom.sparrowwallet.sparrow.io.keycard.Keycardcom.sparrowwallet.sparrow.io.satochip.Satochipexternal Lark/HWI libraryInspect captured patch +90 / −3
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java
index 29a6926..6ac4182 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java
@@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.psbt.PSBT;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@@ -730,13 +731,21 @@ public class DevicePane extends TitledDescriptionPane {
}
}
- importXpub(derivation);
+ importKey(derivation);
});
enumerateService.setOnFailed(workerStateEvent -> {
setError("Error", enumerateService.getException().getMessage());
importButton.setDisable(false);
});
enumerateService.start();
+ } else {
+ importKey(derivation);
+ }
+ }
+
+ private void importKey(List<ChildNumber> derivation) {
+ if(wallet != null && wallet.getPolicyType() == PolicyType.SINGLE_SP) {
+ importSpscan(derivation);
} else {
importXpub(derivation);
}
@@ -771,6 +780,35 @@ public class DevicePane extends TitledDescriptionPane {
getXpubService.start();
}
+ private void importSpscan(List<ChildNumber> derivation) {
+ String derivationPath = KeyDerivation.writePath(derivation);
+
+ Hwi.GetSpscanService getSpscanService = new Hwi.GetSpscanService(device, passphrase.get(), derivationPath);
+ getSpscanService.setOnSucceeded(workerStateEvent -> {
+ SilentPaymentScanAddress spscan = getSpscanService.getValue();
+
+ try {
+ Keystore keystore = new Keystore();
+ keystore.setLabel(device.getModel().toDisplayString());
+ keystore.setSource(KeystoreSource.HW_USB);
+ keystore.setWalletModel(device.getModel());
+ keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
+ keystore.setSilentPaymentScanAddress(spscan);
+
+ importKeystore(derivation, keystore);
+ } catch(Exception e) {
+ setError("Could not retrieve spscan", e.getMessage());
+ }
+ });
+ getSpscanService.setOnFailed(workerStateEvent -> {
+ setError("Could not retrieve spscan", getSpscanService.getException().getMessage());
+ importButton.setDisable(false);
+ });
+ setDescription("Importing...");
+ showHideLink.setVisible(false);
+ getSpscanService.start();
+ }
+
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
if(wallet.getScriptType() == null) {
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
@@ -1179,7 +1217,7 @@ public class DevicePane extends TitledDescriptionPane {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
- importXpub(importDerivation);
+ importKey(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java
index 0b6ecc8..2122e10 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
+import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.DeviceException;
@@ -167,6 +168,20 @@ public class Hwi {
}
}
+ public SilentPaymentScanAddress getSpscan(Device device, String passphrase, String derivationPath) throws ImportException {
+ try {
+ Lark lark = getLark(passphrase);
+ SilentPaymentScanAddress spscan = lark.getSpscanAtPath(device.getType(), device.getPath(), derivationPath);
+ isPromptActive = false;
+ return spscan;
+ } catch(DeviceException e) {
+ throw new ImportException(e.getMessage(), e);
+ } catch(RuntimeException e) {
+ log.error("Error retrieving spscan", e);
+ throw e;
+ }
+ }
+
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor,
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws DisplayAddressException {
try {
@@ -443,6 +458,28 @@ public class Hwi {
}
}
+ public static class GetSpscanService extends Service<SilentPaymentScanAddress> {
+ private final Device device;
+ private final String passphrase;
+ private final String derivationPath;
+
+ public GetSpscanService(Device device, String passphrase, String derivationPath) {
+ this.device = device;
+ this.passphrase = passphrase;
+ this.derivationPath = derivationPath;
+ }
+
+ @Override
+ protected Task<SilentPaymentScanAddress> createTask() {
+ return new Task<>() {
+ protected SilentPaymentScanAddress call() throws ImportException {
+ Hwi hwi = new Hwi();
+ return hwi.getSpscan(device, passphrase, derivationPath);
+ }
+ };
+ }
+ }
+
public static class GetXpubsService extends Service<Map<WalletType, String>> {
private final Device device;
private final String passphrase;
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/Tapsigner.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/Tapsigner.java
index 12ab589..274b19d 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/Tapsigner.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/Tapsigner.java
@@ -54,6 +54,10 @@ public class Tapsigner implements KeystoreCardImport {
@Override
public Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
+ if(policyType == PolicyType.SINGLE_SP) {
+ throw new ImportException(getName() + " does not support receiving silent payments");
+ }
+
if(pin.length() < 6) {
throw new ImportException("PIN too short.");
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/keycard/Keycard.java b/src/main/java/com/sparrowwallet/sparrow/io/keycard/Keycard.java
index dd9924b..cf24fe6 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/keycard/Keycard.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/keycard/Keycard.java
@@ -54,6 +54,10 @@ public class Keycard implements KeystoreCardImport {
@Override
public Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
+ if(policyType == PolicyType.SINGLE_SP) {
+ throw new ImportException(getName() + " does not support receiving silent payments");
+ }
+
if(!StringUtils.isNumeric(pin)) {
throw new ImportException("PIN must be all digits.");
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/satochip/Satochip.java b/src/main/java/com/sparrowwallet/sparrow/io/satochip/Satochip.java
index 29aa6b0..788b4fc 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/satochip/Satochip.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/satochip/Satochip.java
@@ -53,6 +53,10 @@ public class Satochip implements KeystoreCardImport {
@Override
public Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
+ if(policyType == PolicyType.SINGLE_SP) {
+ throw new ImportException(getName() + " does not support receiving silent payments");
+ }
+
if(pin.length() < 4) {
throw new ImportException("PIN too short.");
}
Why this scored 20/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.