gate the display address, sign message and keystore discovery panes on a fingerprint resolved after pin entry
What changed, and why it matters
This commit changes how Sparrow Wallet checks that a connected hardware wallet is the correct one before letting the user display a receiving address, sign a message, or discover keystores. Previously, the app disabled the relevant buttons if the device's reported fingerprint did not match the wallet. The problem is that a PIN-protected device reports no fingerprint until after the PIN is entered, so the old check happened too early and could be bypassed. The new code waits until after PIN entry, re-enumerates the device to get its real fingerprint, and only then enables or disables the button. This prevents a user from accidentally using the wrong hardware wallet and, for example, displaying or signing with keys that do not belong to their wallet.
Review the lark submodule bump (13001e8acf7048a15c81cc050c65e6e164c3aa33) for related security changes, and verify that the onFailed re-enumeration path does not enable operations when the fingerprint could not be resolved. Consider adding explicit user-facing warnings when a fingerprint mismatch is detected after unlock.
Security signals we found
Fingerprint-based device authorization moved from pre-unlock to post-unlock
PIN-protected devices re-enumerated after PIN entry to obtain authoritative fingerprint
Display address, sign message, and keystore discovery now disabled if device fingerprint does not match wallet
Comment explicitly notes risk of silently persisting mismatched keystores during discovery
Potential UI race or silent enablement if re-enumeration fails (onFailed still calls showUnlockedOperation)
Evidence from the diff
DevicePane.java previously gated displayAddressButton, signMessageButton, and discoverKeystoresButton on device.getFingerprint() during pane creation, before PIN unlock. For PIN-protected hardware wallets, getFingerprint() is null until unlocked, so the buttons were not reliably disabled for mismatched devices. The patch removes the early gating, adds post-PIN re-enumeration via Hwi.EnumerateService for DISPLAY_ADDRESS, SIGN_MESSAGE, and DISCOVER_KEYSTORES operations, updates device.setFingerprint() from the fresh enumeration, and moves the fingerprint matching logic into showOperationButton() after unlock. A new showUnlockedOperation() helper consolidates the post-unlock UI flow. The lark submodule is also bumped.
Changed components
src/main/java/com/sparrowwallet/sparrow/control/DevicePane.javalark submoduleInspect captured patch +34 / −14
### lark
@@ -1 +1 @@
-Subproject commit 33fbba951e3a881a96a71f4711777dbc69f9c0ab
+Subproject commit 13001e8acf7048a15c81cc050c65e6e164c3aa33
### src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java
@@ -378,11 +378,6 @@ private void createDisplayAddressButton() {
});
displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty());
displayAddressButton.setVisible(false);
-
- List<String> fingerprints = outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
- if(device.getFingerprint() != null && !fingerprints.contains(device.getFingerprint())) {
- displayAddressButton.setDisable(true);
- }
}
private void createSignMessageButton() {
@@ -394,10 +389,6 @@ private void createSignMessageButton() {
});
signMessageButton.managedProperty().bind(signMessageButton.visibleProperty());
signMessageButton.setVisible(false);
-
- if(device.getFingerprint() != null && !device.getFingerprint().equals(requiredDerivation.getMasterFingerprint())) {
- signMessageButton.setDisable(true);
- }
}
private void createDiscoverKeystoresButton() {
@@ -629,11 +620,26 @@ private void sendPin(String pin) {
setPassphraseButton.setDisable(true);
setContent(getPassphraseEntry());
setExpanded(true);
+ } else if(device.getFingerprint() == null && (deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS) || deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)
+ || deviceOperation.equals(DeviceOperation.DISCOVER_KEYSTORES))) {
+ //A PIN protected device reports no fingerprint until it is unlocked, and these operations gate on it to avoid using the wrong device
+ Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
+ enumerateService.setOnSucceeded(enumerateEvent -> {
+ for(Device freshDevice : enumerateService.getValue()) {
+ if(device.getPath().equals(freshDevice.getPath()) && device.getModel().equals(freshDevice.getModel())) {
+ device.setFingerprint(freshDevice.getFingerprint());
+ }
+ }
+
+ showUnlockedOperation();
+ });
+ enumerateService.setOnFailed(enumerateEvent -> {
+ showUnlockedOperation();
+ setError("Error", enumerateService.getException().getMessage());
+ });
+ enumerateService.start();
} else {
- showOperationButton();
- if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
- setContent(getTogglePassphraseOn());
- }
+ showUnlockedOperation();
}
} else {
setError("Incorrect PIN", null);
@@ -652,6 +658,13 @@ private void sendPin(String pin) {
sendPinService.start();
}
+ private void showUnlockedOperation() {
+ showOperationButton();
+ if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
+ setContent(getTogglePassphraseOn());
+ }
+ }
+
private void sendPassphrase(String passphrase) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase);
enumerateService.setOnSucceeded(workerStateEvent -> {
@@ -1193,16 +1206,23 @@ private void showOperationButton() {
signButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
+ //A device which has not yet been unlocked reports no fingerprint, so this check is only meaningful once the operation button is shown
+ List<String> fingerprints = outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
displayAddressButton.setDefaultButton(defaultDevice);
displayAddressButton.setVisible(true);
+ displayAddressButton.setDisable(device.getFingerprint() != null && !fingerprints.contains(device.getFingerprint()));
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)) {
signMessageButton.setDefaultButton(defaultDevice);
signMessageButton.setVisible(true);
+ signMessageButton.setDisable(device.getFingerprint() != null && !device.getFingerprint().equals(requiredDerivation.getMasterFingerprint()));
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.DISCOVER_KEYSTORES)) {
+ //Discovery stamps the wallet master fingerprint on keystores built from the device xpubs, so a mismatched device is silently persisted
discoverKeystoresButton.setDefaultButton(defaultDevice);
discoverKeystoresButton.setVisible(true);
+ discoverKeystoresButton.setDisable(device.getFingerprint() != null && wallet.getKeystores().size() == 1
+ && !device.getFingerprint().equals(wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()));
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
if(defaultDevice) {Why this scored 59/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.