replace hwi with lark

This commit is contained in:
Craig Raw 2025-01-21 10:00:38 +02:00
parent fb0fd013d9
commit 0e9d97c221
13 changed files with 199 additions and 549 deletions

View file

@ -59,6 +59,7 @@ java {
dependencies {
//Any changes to the dependencies must be reflected in the module definitions below!
implementation(project(':drongo'))
implementation(project(':lark'))
implementation('com.google.guava:guava:33.0.0-jre')
implementation('com.google.code.gson:gson:2.9.1')
implementation('com.h2database:h2:2.1.214')
@ -585,6 +586,14 @@ extraJavaModuleInfo {
module('org.jcommander:jcommander', 'org.jcommander') {
exports('com.beust.jcommander')
}
module('com.sparrowwallet:hid4java', 'org.hid4java') {
requires('com.sun.jna')
exports('org.hid4java')
exports('org.hid4java.jna')
}
module('com.sparrowwallet:usb4java', 'org.usb4java') {
exports('org.usb4java')
}
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
exports('com.jcraft.jzlib')
}

2
drongo

@ -1 +1 @@
Subproject commit 7b9affb3de635de998ae11f542fdd4ebb9ba2b53
Subproject commit 89a6b1296e75508eae498f0199928cff0b8a660c

2
lark

@ -1 +1 @@
Subproject commit 6108d1dbd1d6d6057a8c14fc1539ae07c273ea9a
Subproject commit 9576ffbbeeab87777be42add91855a0221ae55c1

View file

@ -778,7 +778,7 @@ public class DevicePane extends TitledDescriptionPane {
signButton.setDisable(false);
}
} else {
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt, OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName());
signPSBTService.setOnSucceeded(workerStateEvent -> {
PSBT signedPsbt = signPSBTService.getValue();
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
@ -820,7 +820,8 @@ public class DevicePane extends TitledDescriptionPane {
}
private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor,
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName());
displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address));

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.HardwareClient;
import java.util.Objects;
@ -122,4 +123,18 @@ public class Device {
public int hashCode() {
return Objects.hash(type, path);
}
public static Device fromHardwareClient(HardwareClient hardwareClient) {
Device device = new Device();
device.type = hardwareClient.getType();
device.path = hardwareClient.getPath();
device.model = hardwareClient.getModel();
device.needsPinSent = hardwareClient.needsPinSent();
device.needsPassphraseSent = hardwareClient.needsPassphraseSent();
device.fingerprint = hardwareClient.fingerprint();
device.card = hardwareClient.card();
device.warnings = hardwareClient.warnings();
device.error = hardwareClient.error();
return device;
}
}

View file

@ -1,48 +1,43 @@
package com.sparrowwallet.sparrow.io;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.gson.*;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.ExtendedKey;
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.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.DeviceException;
import com.sparrowwallet.lark.Lark;
import com.sparrowwallet.lark.bitbox02.BitBoxFileNoiseConfig;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException;
import javax.smartcardio.CardNotPresentException;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
public class Hwi {
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
private static final String HWI_HOME_DIR = "hwi";
private static final String HWI_VERSION_PREFIX = "hwi-";
private static final String HWI_VERSION = "3.1.0";
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
private static boolean isPromptActive = false;
static {
//deleteHwiDir();
}
public List<Device> enumerate(String passphrase) throws ImportException {
List<Device> devices = new ArrayList<>();
devices.addAll(enumerateUsb(passphrase));
@ -51,45 +46,13 @@ public class Hwi {
}
private List<Device> enumerateUsb(String passphrase) throws ImportException {
String output = null;
try {
List<String> command;
if(passphrase != null) {
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString()));
} else {
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString()));
}
addChainType(command, true);
Lark lark = getLark(passphrase);
isPromptActive = true;
output = execute(command);
Device[] devices = getGson().fromJson(output, Device[].class);
if(devices == null) {
throw new ImportException("Error scanning, check devices are ready");
}
//Restore previous (pre v2.2.0) behaviour for Trezor One - don't default to an empty passphrase if one is not supplied
Arrays.stream(devices).filter(device -> device.containsWarning("Using default passphrase of the empty string")).forEach(device -> {
device.setFingerprint(null);
device.setNeedsPassphraseSent(true);
device.setError("Passphrase needs to be specified before the fingerprint information can be retrieved");
});
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
} catch(IOException e) {
log.error("Error executing " + HWI_VERSION_DIR, e);
throw new ImportException("Error executing HWI", e);
return lark.enumerate().stream().map(Device::fromHardwareClient).toList();
} catch(Exception e) {
if(output != null) {
try {
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
JsonElement error = result.get("error");
throw new ImportException(error.getAsString());
} catch(Exception ex) {
log.error("Error parsing JSON: " + output, e);
throw new ImportException("Error scanning" + (output.isEmpty() ? ", check devices are ready" : ": " + output), e);
}
}
throw e;
log.error("Error enumerating USB devices", e);
throw new ImportException("Error scanning" + (e.getMessage() == null || e.getMessage().isEmpty() ? ", check devices are ready" : ": " + e.getMessage()), e);
} finally {
isPromptActive = false;
}
@ -124,31 +87,43 @@ public class Hwi {
public boolean promptPin(Device device) throws ImportException {
try {
String output = execute(getDeviceCommand(device, Command.PROMPT_PIN));
Lark lark = getLark();
boolean result = lark.promptPin(device.getType(), device.getPath());
isPromptActive = true;
return wasSuccessful(output);
} catch(IOException e) {
throw new ImportException(e);
return result;
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error prompting pin", e);
throw e;
}
}
public boolean sendPin(Device device, String pin) throws ImportException {
try {
String output = execute(getDeviceCommand(device, Command.SEND_PIN, pin));
Lark lark = getLark();
boolean result = lark.sendPin(device.getType(), device.getPath(), pin);
isPromptActive = false;
return wasSuccessful(output);
} catch(IOException e) {
throw new ImportException(e);
return result;
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error sending pin", e);
throw e;
}
}
public boolean togglePassphrase(Device device) throws ImportException {
try {
String output = execute(getDeviceCommand(device, Command.TOGGLE_PASSPHRASE));
Lark lark = getLark();
boolean result = lark.togglePassphrase(device.getType(), device.getPath());
isPromptActive = false;
return wasSuccessful(output);
} catch(IOException e) {
throw new ImportException(e);
return result;
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error toggling passphrase", e);
throw e;
}
}
@ -163,59 +138,33 @@ public class Hwi {
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
try {
String output;
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.GET_XPUB, derivationPath));
} else {
output = execute(getDeviceCommand(device, Command.GET_XPUB, derivationPath));
}
Lark lark = getLark(passphrase);
ExtendedKey xpub = lark.getPubKeyAtPath(device.getType(), device.getPath(), derivationPath);
isPromptActive = false;
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("xpub") != null) {
return result.get("xpub").getAsString();
} else {
JsonElement error = result.get("error");
if(error != null) {
throw new ImportException(error.getAsString());
} else {
throw new ImportException("Could not retrieve xpub - reconnect your device and try again.");
}
}
} catch(IOException e) {
throw new ImportException(e);
return xpub.toString();
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error retrieving xpub", e);
throw e;
}
}
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor,
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws DisplayAddressException {
try {
if(!Arrays.asList(ScriptType.ADDRESSABLE_TYPES).contains(scriptType)) {
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only addressable types supported");
}
String descriptor = outputDescriptor.toString();
isPromptActive = true;
String output;
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--desc", descriptor));
} else {
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--desc", descriptor));
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("address") != null) {
return result.get("address").getAsString();
} else {
JsonElement error = result.get("error");
if(error != null) {
throw new DisplayAddressException(error.getAsString());
} else {
throw new DisplayAddressException("Could not retrieve address");
}
}
} catch(IOException e) {
throw new DisplayAddressException(e);
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
return lark.displayAddress(device.getType(), device.getPath(), addressDescriptor);
} catch(DeviceException e) {
throw new DisplayAddressException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error displaying address", e);
throw e;
} finally {
isPromptActive = false;
}
@ -224,406 +173,73 @@ public class Hwi {
public String signMessage(Device device, String passphrase, String message, String derivationPath) throws SignMessageException {
try {
isPromptActive = true;
String output;
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
} else {
output = execute(getDeviceArguments(device, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("signature") != null) {
return result.get("signature").getAsString();
} else {
JsonElement error = result.get("error");
if(error != null) {
throw new SignMessageException(error.getAsString());
} else {
throw new SignMessageException("Could not sign message");
}
}
} catch(IOException e) {
throw new SignMessageException(e);
Lark lark = getLark(passphrase);
return lark.signMessage(device.getType(), device.getPath(), message, derivationPath);
} catch(DeviceException e) {
throw new SignMessageException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error signing message", e);
throw e;
} finally {
isPromptActive = false;
}
}
public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException {
public PSBT signPSBT(Device device, String passphrase, PSBT psbt,
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws SignTransactionException {
try {
String psbtBase64 = psbt.toBase64String();
isPromptActive = true;
String output;
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
} else {
output = execute(getDeviceArguments(device, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("psbt") != null) {
String strPsbt = result.get("psbt").getAsString();
return PSBT.fromString(strPsbt);
} else {
JsonElement error = result.get("error");
if(error != null && error.getAsString().equals("sign_tx canceled")) {
throw new SignTransactionException("Signing cancelled");
} else if(error != null && error.getAsString().equals("open failed")) {
throw new SignTransactionException("Please reconnect your USB device");
} else if(error != null) {
throw new SignTransactionException(error.getAsString());
} else {
throw new SignTransactionException("Could not retrieve PSBT");
}
}
} catch(IOException e) {
throw new SignTransactionException("Could not sign PSBT", e);
} catch(PSBTParseException e) {
throw new SignTransactionException("Could not parse signed PSBT" + (e.getMessage() != null ? ": " + e.getMessage() : ""), e);
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
return lark.signTransaction(device.getType(), device.getPath(), psbt);
} catch(DeviceException e) {
throw new SignTransactionException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error signing PSBT", e);
throw e;
} finally {
isPromptActive = false;
}
}
private String execute(List<String> command) throws IOException {
long start = System.currentTimeMillis();
Process process = null;
try {
ProcessBuilder processBuilder = new ProcessBuilder(command);
process = processBuilder.start();
return getProcessOutput(process);
} finally {
deleteExtractionOnFailure(process, start);
}
private Lark getLark() {
return getLark(null);
}
private String execute(List<String> arguments, Command command, String... commandArguments) throws IOException {
long start = System.currentTimeMillis();
Process process = null;
try {
List<String> processArguments = new ArrayList<>(arguments);
private Lark getLark(String passphrase) {
return getLark(passphrase, null, null, null);
}
boolean useStdin = Arrays.stream(commandArguments).noneMatch(arg -> arg.contains("\n"));
if(useStdin) {
processArguments.add("--stdin");
private Lark getLark(String passphrase, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
Lark lark = new Lark(AppServices.getHttpClientService());
lark.setBitBoxNoiseConfig(new BitBoxFxNoiseConfig());
if(passphrase != null) {
lark.setPassphrase(passphrase);
}
if(walletDescriptor != null && walletName != null) {
if(walletRegistration != null) {
lark.addWalletRegistration(walletDescriptor, walletName, walletRegistration);
} else {
processArguments.add(command.toString());
processArguments.addAll(Arrays.asList(commandArguments));
lark.addWalletName(walletDescriptor, walletName);
}
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
process = processBuilder.start();
if(useStdin) {
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
writer.write(command.toString());
for(String commandArgument : commandArguments) {
writer.write(" \"");
writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\""));
writer.write("\"");
}
writer.flush();
}
}
return getProcessOutput(process);
} finally {
deleteExtractionOnFailure(process, start);
}
return lark;
}
private String getProcessOutput(Process process) throws IOException {
String output;
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
output = CharStreams.toString(reader);
}
if(output.isEmpty() && process.getErrorStream() != null) {
try(InputStreamReader reader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) {
String errorOutput = CharStreams.toString(reader);
if(!errorOutput.isEmpty()) {
throw new IOException(errorOutput);
}
}
}
return output;
}
private synchronized File getHwiExecutable(Command command) {
File hwiExecutable = Config.get().getHwi();
if(hwiExecutable != null && hwiExecutable.exists()) {
File homeDir = getHwiHomeDir();
String tmpDir = System.getProperty("java.io.tmpdir");
String hwiPath = hwiExecutable.getAbsolutePath();
if(command.isTestFirst() && (hwiPath.contains(tmpDir) || hwiPath.startsWith(homeDir.getAbsolutePath())) && (!hwiPath.contains(HWI_VERSION_DIR) || !testHwi(hwiExecutable))) {
if(OsType.getCurrent() == OsType.MACOS) {
IOUtils.deleteDirectory(hwiExecutable.getParentFile());
} else {
hwiExecutable.delete();
}
}
}
if(hwiExecutable == null || !hwiExecutable.exists()) {
try {
OsType osType = OsType.getCurrent();
String osArch = System.getProperty("os.arch");
Set<PosixFilePermission> ownerExecutableWritable = PosixFilePermissions.fromString("rwxr--r--");
//A PyInstaller --onefile expands into a new directory on every run triggering OSX Gatekeeper checks.
//To avoid doing these with every invocation, use a --onedir packaging and expand into a temp folder on OSX
//The check will still happen on first invocation, but will not thereafter
//See https://github.com/bitcoin-core/HWI/issues/327 for details
if(osType == OsType.MACOS) {
InputStream inputStream;
if(osArch.equals("aarch64")) {
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.tar.bz2");
} else {
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.tar.bz2");
}
if(inputStream == null) {
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
}
File hwiHomeDir = getHwiHomeDir();
File hwiVersionDir = new File(hwiHomeDir, HWI_VERSION_DIR);
IOUtils.deleteDirectory(hwiVersionDir);
if(!hwiVersionDir.exists()) {
Files.createDirectories(hwiVersionDir.toPath(), PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
}
log.debug("Using HWI path: " + hwiVersionDir.getAbsolutePath());
File hwiExec = null;
try(TarArchiveInputStream tarInput = new TarArchiveInputStream(new BZip2CompressorInputStream(inputStream))) {
TarArchiveEntry tarEntry = tarInput.getNextEntry();
while(tarEntry != null) {
if(tarEntry.isDirectory()) {
newDirectory(hwiVersionDir, tarEntry, ownerExecutableWritable);
} else if(tarEntry.isSymbolicLink()) {
newSymlink(hwiVersionDir, tarEntry, ownerExecutableWritable);
} else {
File newFile = newFile(hwiVersionDir, tarEntry, ownerExecutableWritable);
try(FileOutputStream fos = new FileOutputStream(newFile)) {
ByteStreams.copy(tarInput, new FileOutputStream(newFile));
fos.flush();
}
if(tarEntry.getName().equals("hwi")) {
hwiExec = newFile;
}
}
tarEntry = tarInput.getNextEntry();
}
}
hwiExecutable = hwiExec;
} else {
InputStream inputStream;
Path tempExecPath;
if(osType == OsType.WINDOWS) {
Files.createDirectories(getHwiHomeDir().toPath());
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
tempExecPath = Files.createTempFile(getHwiHomeDir().toPath(), HWI_VERSION_DIR, null);
} else if(osArch.equals("aarch64")) {
inputStream = Hwi.class.getResourceAsStream("/native/linux/aarch64/hwi");
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
} else {
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
}
if(inputStream == null) {
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
}
File tempExec = tempExecPath.toFile();
tempExec.deleteOnExit();
try(OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec))) {
ByteStreams.copy(inputStream, tempExecStream);
inputStream.close();
tempExecStream.flush();
};
hwiExecutable = tempExec;
}
} catch(Exception e) {
log.error("Error initializing HWI", e);
}
Config.get().setHwi(hwiExecutable);
}
return hwiExecutable;
}
private File getHwiHomeDir() {
if(OsType.getCurrent() == OsType.MACOS || OsType.getCurrent() == OsType.WINDOWS) {
return new File(Storage.getSparrowDir(), HWI_HOME_DIR);
}
return new File(System.getProperty("java.io.tmpdir"));
}
private boolean testHwi(File hwiExecutable) {
private static void deleteHwiDir() {
try {
List<String> command = List.of(hwiExecutable.getAbsolutePath(), "--version");
ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = processBuilder.start();
int exitValue = process.waitFor();
return exitValue == 0;
} catch (Exception e) {
return false;
}
}
public static File newDirectory(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, tarEntry.getName());
File destDir = Files.createDirectory(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destSubDirPath = destDir.getCanonicalPath();
if(!destSubDirPath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
}
return destDir;
}
public static File newFile(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, tarEntry.getName());
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destFilePath = destFile.getCanonicalPath();
if(!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
}
return destFile;
}
public static File newSymlink(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, tarEntry.getName());
File destFile = Files.createSymbolicLink(path, Paths.get(tarEntry.getLinkName())).toFile();
String destFilePath = destFile.getCanonicalPath();
if(!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
}
return destFile;
}
private boolean wasSuccessful(String output) throws ImportException {
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("error") != null) {
throw new ImportException(result.get("error").getAsString());
}
return result.get("success").getAsBoolean();
}
private void deleteExtractionOnFailure(Process process, long after) {
try {
if(OsType.getCurrent() != OsType.MACOS && process != null && process.waitFor(100, TimeUnit.MILLISECONDS) && process.exitValue() != 0) {
File extraction = getTemporaryExtraction(after);
if(extraction != null) {
IOUtils.deleteDirectory(extraction);
if(OsType.getCurrent() == OsType.MACOS || OsType.getCurrent() == OsType.WINDOWS) {
File hwiHomeDir = new File(Storage.getSparrowDir(), HWI_HOME_DIR);
if(hwiHomeDir.exists()) {
IOUtils.deleteDirectory(hwiHomeDir);
}
}
} catch(Exception e) {
log.debug("Error deleting temporary extraction", e);
log.error("Error deleting hwi directory", e);
}
}
private File getTemporaryExtraction(long after) {
File tmpDir = new File(System.getProperty("java.io.tmpdir"));
if(!tmpDir.exists()) {
return null;
}
File[] tmps = tmpDir.listFiles(file -> {
if(!file.isDirectory() || file.lastModified() < after) {
return false;
}
String name = file.getName();
if(name.length() < 9 || !name.startsWith("_MEI")) {
return false;
}
File hwilib = new File(file, "hwilib");
return hwilib.exists();
});
return tmps == null || tmps.length == 0 ? null : Arrays.stream(tmps).sorted(Comparator.comparingLong(File::lastModified)).findFirst().orElse(null);
}
private List<String> getDeviceArguments(Device device, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType()));
addChainType(elements, false);
return elements;
}
private List<String> getDeviceArguments(Device device, String passphrase, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase)));
addChainType(elements, false);
return elements;
}
private List<String> getDeviceCommand(Device device, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
addChainType(elements, true);
return elements;
}
private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
addChainType(elements, true);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
}
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase), command.toString()));
addChainType(elements, true);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
}
private void addChainType(List<String> elements, boolean commandPresent) {
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - (commandPresent ? 1 : 0), "--chain");
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.getCanonical()));
}
}
private String getChainName(Network network) {
if(network == Network.MAINNET) {
return "main";
} else if(network == Network.TESTNET) {
return "test";
}
return network.toString();
}
private String escape(String passphrase) {
OsType osType = OsType.getCurrent();
if(osType == OsType.WINDOWS) {
return passphrase.replace("\"", "\\\"");
}
return passphrase;
}
public static class EnumerateService extends Service<List<Device>> {
private final String passphrase;
@ -724,13 +340,29 @@ public class Hwi {
private final Device device;
private final String passphrase;
private final ScriptType scriptType;
private final OutputDescriptor outputDescriptor;
private final OutputDescriptor addressDescriptor;
private final OutputDescriptor walletDescriptor;
private final String walletName;
private final byte[] walletRegistration;
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) {
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor, OutputDescriptor walletDescriptor, String walletName) {
this.device = device;
this.passphrase = passphrase;
this.scriptType = scriptType;
this.outputDescriptor = outputDescriptor;
this.addressDescriptor = addressDescriptor;
this.walletDescriptor = walletDescriptor;
this.walletName = walletName;
this.walletRegistration = null;
}
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
this.device = device;
this.passphrase = passphrase;
this.scriptType = scriptType;
this.addressDescriptor = addressDescriptor;
this.walletDescriptor = walletDescriptor;
this.walletName = walletName;
this.walletRegistration = walletRegistration;
}
@Override
@ -738,7 +370,7 @@ public class Hwi {
return new Task<>() {
protected String call() throws DisplayAddressException {
Hwi hwi = new Hwi();
return hwi.displayAddress(device, passphrase, scriptType, outputDescriptor);
return hwi.displayAddress(device, passphrase, scriptType, addressDescriptor, walletDescriptor, walletName, walletRegistration);
}
};
}
@ -816,11 +448,26 @@ public class Hwi {
private final Device device;
private final String passphrase;
private final PSBT psbt;
private final OutputDescriptor walletDescriptor;
private final String walletName;
private final byte[] walletRegistration;
public SignPSBTService(Device device, String passphrase, PSBT psbt) {
public SignPSBTService(Device device, String passphrase, PSBT psbt, OutputDescriptor walletDescriptor, String walletName) {
this.device = device;
this.passphrase = passphrase;
this.psbt = psbt;
this.walletDescriptor = walletDescriptor;
this.walletName = walletName;
this.walletRegistration = null;
}
public SignPSBTService(Device device, String passphrase, PSBT psbt, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
this.device = device;
this.passphrase = passphrase;
this.psbt = psbt;
this.walletDescriptor = walletDescriptor;
this.walletName = walletName;
this.walletRegistration = walletRegistration;
}
@Override
@ -828,72 +475,48 @@ public class Hwi {
return new Task<>() {
protected PSBT call() throws SignTransactionException {
Hwi hwi = new Hwi();
return hwi.signPSBT(device, passphrase, psbt);
return hwi.signPSBT(device, passphrase, psbt, walletDescriptor, walletName, walletRegistration);
}
};
}
}
public Gson getGson() {
GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelSerializer());
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelDeserializer());
return gsonBuilder.create();
}
private static class DeviceModelSerializer implements JsonSerializer<WalletModel> {
@Override
public JsonElement serialize(WalletModel src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString().toLowerCase(Locale.ROOT));
private static final class BitBoxFxNoiseConfig extends BitBoxFileNoiseConfig {
public BitBoxFxNoiseConfig() {
super(Path.of(Storage.getSparrowDir().getAbsolutePath(), "lark", "bitbox02.json").toFile());
}
}
private static class DeviceModelDeserializer implements JsonDeserializer<WalletModel> {
@Override
public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
String modelStr = json.getAsJsonPrimitive().getAsString();
try {
return WalletModel.valueOf(modelStr.toUpperCase(Locale.ROOT).replace(' ', '_'));
} catch(Exception e) {
for(WalletModel model : WalletModel.values()) {
if(modelStr.startsWith(model.getType())) {
return model;
}
public boolean showPairing(String code, DeviceResponse response) throws DeviceException {
CountDownLatch latch = new CountDownLatch(2);
AtomicBoolean confirmedDevice = new AtomicBoolean(false);
AtomicBoolean confirmedApp = new AtomicBoolean(false);
Thread showPairingDeviceThread = new Thread(() -> {
try {
confirmedDevice.set(response.call());
latch.countDown();
} catch(DeviceException e) {
throw new RuntimeException(e);
}
});
showPairingDeviceThread.start();
Platform.runLater(() -> {
Optional<ButtonType> optConfirm = AppServices.showAlertDialog("Confirm Pairing", "Confirm the following code is shown on BitBox02:\n\n" + code, Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
if(optConfirm.isPresent() && optConfirm.get() == ButtonType.YES) {
confirmedApp.set(true);
}
latch.countDown();
});
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
throw new IllegalArgumentException("Could not determine wallet model for " + modelStr);
}
}
private enum Command {
ENUMERATE("enumerate", true),
PROMPT_PIN("promptpin", true),
SEND_PIN("sendpin", false),
TOGGLE_PASSPHRASE("togglepassphrase", true),
DISPLAY_ADDRESS("displayaddress", true),
SIGN_MESSAGE("signmessage", true),
GET_XPUB("getxpub", true),
SIGN_TX("signtx", true);
private final String command;
private final boolean testFirst;
Command(String command, boolean testFirst) {
this.command = command;
this.testFirst = testFirst;
}
public String getCommand() {
return command;
}
public boolean isTestFirst() {
return testFirst;
}
@Override
public String toString() {
return command;
return confirmedDevice.get() && confirmedApp.get();
}
}
}

View file

@ -233,7 +233,8 @@ public class ReceiveController extends WalletFormController implements Initializ
dlg.showAndWait();
} else {
Device actualDevice = possibleDevices.get(0);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor,
OutputDescriptor.getOutputDescriptor(walletForm.getWallet()), walletForm.getWallet().getFullName());
displayAddressService.setOnFailed(failedEvent -> {
Platform.runLater(() -> {
DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor);

View file

@ -65,4 +65,5 @@ open module com.sparrowwallet.sparrow {
requires java.smartcardio;
requires com.jcraft.jzlib;
requires com.sparrowwallet.tern;
requires com.sparrowwallet.lark;
}