diff --git a/build.gradle b/build.gradle index a82a3927..14eddb62 100644 --- a/build.gradle +++ b/build.gradle @@ -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') } diff --git a/drongo b/drongo index 7b9affb3..89a6b129 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 7b9affb3de635de998ae11f542fdd4ebb9ba2b53 +Subproject commit 89a6b1296e75508eae498f0199928cff0b8a660c diff --git a/lark b/lark index 6108d1db..9576ffbb 160000 --- a/lark +++ b/lark @@ -1 +1 @@ -Subproject commit 6108d1dbd1d6d6057a8c14fc1539ae07c273ea9a +Subproject commit 9576ffbbeeab87777be42add91855a0221ae55c1 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index e8122641..2897ee80 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -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)); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Device.java b/src/main/java/com/sparrowwallet/sparrow/io/Device.java index bc066103..584167bd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Device.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Device.java @@ -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; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java index c8178dae..4be64397 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java @@ -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 enumerate(String passphrase) throws ImportException { List devices = new ArrayList<>(); devices.addAll(enumerateUsb(passphrase)); @@ -51,45 +46,13 @@ public class Hwi { } private List enumerateUsb(String passphrase) throws ImportException { - String output = null; try { - List 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 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 arguments, Command command, String... commandArguments) throws IOException { - long start = System.currentTimeMillis(); - Process process = null; - try { - List 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 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 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 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 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 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 getDeviceArguments(Device device, Command command) throws IOException { - List elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType())); - addChainType(elements, false); - return elements; - } - - private List getDeviceArguments(Device device, String passphrase, Command command) throws IOException { - List 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 getDeviceCommand(Device device, Command command) throws IOException { - List 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 getDeviceCommand(Device device, Command command, String... commandData) throws IOException { - List 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 getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException { - List 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 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> { 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 { - @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 { @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 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(); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java index 6003d1f1..9ec6fe89 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java @@ -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); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 0e97b69b..eba562f2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -65,4 +65,5 @@ open module com.sparrowwallet.sparrow { requires java.smartcardio; requires com.jcraft.jzlib; requires com.sparrowwallet.tern; + requires com.sparrowwallet.lark; } \ No newline at end of file diff --git a/src/main/resources/native/linux/aarch64/hwi b/src/main/resources/native/linux/aarch64/hwi deleted file mode 100755 index bf3444ba..00000000 Binary files a/src/main/resources/native/linux/aarch64/hwi and /dev/null differ diff --git a/src/main/resources/native/linux/x64/hwi b/src/main/resources/native/linux/x64/hwi deleted file mode 100755 index 8f1aefba..00000000 Binary files a/src/main/resources/native/linux/x64/hwi and /dev/null differ diff --git a/src/main/resources/native/osx/aarch64/hwi-3.1.0-mac-aarch64-signed.tar.bz2 b/src/main/resources/native/osx/aarch64/hwi-3.1.0-mac-aarch64-signed.tar.bz2 deleted file mode 100644 index 67f0de43..00000000 Binary files a/src/main/resources/native/osx/aarch64/hwi-3.1.0-mac-aarch64-signed.tar.bz2 and /dev/null differ diff --git a/src/main/resources/native/osx/x64/hwi-3.1.0-mac-amd64-signed.tar.bz2 b/src/main/resources/native/osx/x64/hwi-3.1.0-mac-amd64-signed.tar.bz2 deleted file mode 100755 index c69ad427..00000000 Binary files a/src/main/resources/native/osx/x64/hwi-3.1.0-mac-amd64-signed.tar.bz2 and /dev/null differ diff --git a/src/main/resources/native/windows/x64/hwi.exe b/src/main/resources/native/windows/x64/hwi.exe deleted file mode 100755 index bc038d0f..00000000 Binary files a/src/main/resources/native/windows/x64/hwi.exe and /dev/null differ