initial commit

This commit is contained in:
Craig Raw 2024-11-25 14:00:49 +02:00
commit 7e803996da
173 changed files with 214394 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.idea
.gradle
*iml
build
/*.properties
out
*.log
.DS_Store

177
LICENSE Normal file
View File

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# Lark
Lark is Java library for interacting with USB hardware wallets in Bitcoin related functions.
The initial implementation is a port of the Python library [HWI](https://github.com/bitcoin-core/HWI), but the library has since been extended to support additional functionality.
The following hardware wallets (for all models) are supported:
- Coldcard
- Trezor
- Ledger
- Bitbox02
- Jade
- Keepkey
## Example usage
```java
Lark lark = new Lark();
List<HardwareClient> clients = lark.enumerate();
for(HardwareClient client : clients) {
ExtendedKey xpub = lark.getPubKeyAtPath(client.getType(), client.getPath(), "m/84'/1'/0'");
}
```

36
build.gradle Normal file
View File

@ -0,0 +1,36 @@
plugins {
id 'java'
}
group = 'com.sparrowwallet'
version = '0.9'
repositories {
mavenCentral()
maven { url 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' }
}
java {
withSourcesJar()
}
dependencies {
implementation('org.hid4java:hid4java:0.8.0')
implementation('com.fazecast:jSerialComm:2.11.0')
implementation('org.usb4java:usb4java:1.3.0')
implementation('co.nstant.in:cbor:0.9')
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('com.google.guava:guava:33.0.0-jre')
implementation('com.google.protobuf:protobuf-java:4.28.3')
implementation('commons-codec:commons-codec:1.17.1')
implementation('org.jcommander:jcommander:2.0')
implementation('org.slf4j:slf4j-api:2.0.12')
implementation('com.sparrowwallet:tern:1.0.2')
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation('org.junit.jupiter:junit-jupiter')
}
test {
useJUnitPlatform()
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Thu Nov 21 14:18:54 SAST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Executable file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

2
settings.gradle Normal file
View File

@ -0,0 +1,2 @@
rootProject.name = 'lark'

View File

@ -0,0 +1,840 @@
package com.sparrowwallet.lark;
import com.google.protobuf.ByteString;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.bitbox02.*;
import com.sparrowwallet.lark.bitbox02.generated.Antiklepto;
import com.sparrowwallet.lark.bitbox02.generated.Btc;
import com.sparrowwallet.lark.bitbox02.generated.Common;
import com.sparrowwallet.lark.bitbox02.generated.Hww;
import org.hid4java.HidDevice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.*;
import static com.sparrowwallet.lark.bitbox02.BitBox02Device.*;
public class BitBox02Client extends HardwareClient {
private static final Logger log = LoggerFactory.getLogger(BitBox02Client.class);
private static final DeviceId BITBOX02_ID = new DeviceId(BITBOX02_VID, BITBOX02_PID);
private final HidDevice hidDevice;
private final BitBox02Edition edition;
private BitBoxNoiseConfig noiseConfig = new BitBoxAppNoiseConfig();
private String masterFingerprint;
private static final SecureRandom secureRandom = new SecureRandom();
private final ChildNumber PURPOSE_P2WPKH = ScriptType.P2WPKH.getDefaultDerivation().get(0);
private final ChildNumber PURPOSE_P2WPKH_P2SH = ScriptType.P2SH_P2WPKH.getDefaultDerivation().get(0);
public BitBox02Client(HidDevice hidDevice) throws DeviceException {
if(BITBOX02_ID.matches(hidDevice) && (hidDevice.getUsagePage() == 0xFFFF || hidDevice.getInterfaceNumber() == 0) &&
(BitBox02Edition.fromProductString(hidDevice.getProduct()) != null)) {
this.hidDevice = hidDevice;
this.edition = BitBox02Edition.fromProductString(hidDevice.getProduct());
} else {
throw new DeviceException("Not a BitBox02");
}
}
@Override
void initializeMasterFingerprint() throws DeviceException {
try(BitBox02Device bitBox02Device = new BitBox02Device(hidDevice, new U2FHid(new HidPhysicalLayer(hidDevice)), noiseConfig)) {
initializeMasterFingerprint(bitBox02Device);
}
}
private void initializeMasterFingerprint(BitBox02Device bitBox02Device) throws DeviceException {
if(masterFingerprint == null) {
Hww.Request.Builder request = Hww.Request.newBuilder();
request.setFingerprint(Common.RootFingerprintRequest.newBuilder().build());
Hww.Response hwwResponse = bitBox02Device.msgQuery(request.build(), Hww.Response.ResponseCase.FINGERPRINT);
this.masterFingerprint = Utils.bytesToHex(hwwResponse.getFingerprint().getFingerprint().toByteArray());
}
}
/**
* Fetch the public key at the derivation path.
*
* The BitBox02 has strict keypath validation.
*
* The only accepted keypaths for xpubs are (as of firmware v9.4.0):
*
* - `m/49'/0'/<account'>` for `p2wpkh-p2sh` (segwit wrapped in P2SH)
* - `m/84'/0'/<account'>` for `p2wpkh` (native segwit v0)
* - `m/86'/0'/<account'>` for `p2tr` (native segwit v1)
* - `m/48'/0'/<account'>/2'` for p2wsh multisig (native segwit v0 multisig).
* - `m/48'/0'/<account'>/1'` for p2wsh-p2sh multisig (p2sh-wrapped segwit v0 multisig).
* - `m/48'/0'/<account'>` for p2wsh and p2wsh-p2sh multisig.
*
* `account'` can be between `0'` and `99'`.
*
* For address keypaths, append `/0/<address index>` for a receive and `/1/<change index>` for a change
* address. Up to `10000` addresses are supported.
*
* In testnet mode, the second element must be `1'` (e.g. `m/49'/1'/...`).
*
* Public keys for the Legacy address type (i.e. P2PKH and P2SH multisig) derivation path are unsupported.
*
* @param path the derivation path
* @return the xpub at the derivation path
* @throws DeviceException if an error occurs
*/
@Override
ExtendedKey getPubKeyAtPath(String path) throws DeviceException {
try(BitBox02Device bitBox02Device = new BitBox02Device(hidDevice, new U2FHid(new HidPhysicalLayer(hidDevice)), noiseConfig)) {
Hww.Request.Builder request = Hww.Request.newBuilder();
request.setBtcPub(Btc.BTCPubRequest.newBuilder().setCoin(getCoin()).addAllKeypath(KeyDerivation.parsePath(path).stream().map(ChildNumber::i).toList())
.setXpubType(Network.get() == Network.MAINNET ? Btc.BTCPubRequest.XPubType.XPUB : Btc.BTCPubRequest.XPubType.TPUB).setDisplay(false));
Hww.Response hwwResponse = bitBox02Device.msgQuery(request.build(), null);
return ExtendedKey.fromDescriptor(hwwResponse.getPub().getPub());
}
}
/**
* Sign a transaction with the BitBox02.
*
* The BitBox02 allows mixing inputs of different script types (e.g. and `p2wpkh-p2sh` `p2wpkh`), as
* long as the keypaths use the appropriate bip44 purpose field per input (e.g. `49'` and `84'`) and
* all account indexes are the same.
*
* Transactions with legacy inputs are not supported.
* @param psbt the PSBT to sign
* @return the signed PSBT
* @throws DeviceException if an error occurs
*/
@Override
PSBT signTransaction(PSBT psbt) throws DeviceException {
try(BitBox02Device bitBox02Device = new BitBox02Device(hidDevice, new U2FHid(new HidPhysicalLayer(hidDevice)), noiseConfig)) {
initializeMasterFingerprint(bitBox02Device);
List<Btc.BTCScriptConfigWithKeypath> scriptConfigs = new ArrayList<>();
List<TxInput> inputs = new ArrayList<>();
Integer bip44Account = null;
/* One pubkey per input. The pubkey identifies the key per input with which we sign. There
must be exactly one pubkey per input that belongs to the BitBox02. */
List<ECKey> foundPubKeys = new ArrayList<>();
for(int i = 0; i < psbt.getPsbtInputs().size(); i++) {
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
if(psbtInput.getSigHash() != null && psbtInput.getSigHash() != SigHash.ALL && psbtInput.getSigHash() != SigHash.DEFAULT) {
throw new DeviceException("The BitBox02 only supports SIGHASH_ALL or SIGHASH_DEFAULT. Found sighash: " + psbtInput.getSigHash());
}
TransactionOutput utxo = null;
Transaction prevTx = null;
/* psbt_in.witness_utxo was originally used for segwit utxo's, but since it was
discovered that the amounts are not correctly committed to in the segwit sighash, the
full prevtx (non_witness_utxo) is supplied for both segwit and non-segwit inputs.
See
- https://medium.com/shiftcrypto/bitbox-app-firmware-update-6-2020-c70f733a5330
- https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd.
- https://github.com/zkSNACKs/WalletWasabi/pull/3822
The BitBox02 requires all prevtxs if not all of the inputs are taproot. */
if(psbtInput.getNonWitnessUtxo() != null) {
utxo = psbtInput.getNonWitnessUtxo().getOutputs().get((int)psbtInput.getInput().getOutpoint().getIndex());
prevTx = psbtInput.getNonWitnessUtxo();
} else if(psbtInput.getWitnessUtxo() != null) {
utxo = psbtInput.getWitnessUtxo();
}
if(utxo == null) {
throw new DeviceException("No utxo found for input " + i);
}
Map<ECKey, KeyDerivation> derivedPubKeys = new HashMap<>(psbtInput.getDerivedPublicKeys());
if(psbtInput.getTapInternalKey() != null) {
Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys = psbtInput.getTapDerivedPublicKeys();
if(tapDerivedPublicKeys != null) {
for(ECKey ecKey : tapDerivedPublicKeys.keySet()) {
Map<KeyDerivation, List<Sha256Hash>> tapKeyDerivationMap = tapDerivedPublicKeys.get(ecKey);
for(KeyDerivation keyDerivation : tapKeyDerivationMap.keySet()) {
if(!tapKeyDerivationMap.get(keyDerivation).isEmpty()) {
throw new DeviceException("The BitBox02 does not support Taproot script path spending. Found " + tapKeyDerivationMap.size() + " leaf hashes");
}
derivedPubKeys.put(ecKey, keyDerivation);
break;
}
}
}
}
Optional<Map.Entry<ECKey, KeyDerivation>> optOurKey = findOurKey(bitBox02Device, derivedPubKeys);
if(optOurKey.isEmpty()) {
throw new DeviceException("No key found for input " + i);
}
foundPubKeys.add(optOurKey.get().getKey());
KeyDerivation keyDerivation = optOurKey.get().getValue();
int inputAccount = psbtInput.getScriptType().getAccount(KeyDerivation.writePath(keyDerivation.getDerivation().subList(0, keyDerivation.getDerivation().size() - 2)));
if(bip44Account == null) {
bip44Account = inputAccount;
} else if(bip44Account != inputAccount) {
throw new DeviceException("The bip44 account index must be the same for all inputs and changes");
}
int scriptConfigIndex = addScriptConfig(scriptConfigs, getScriptConfigFromUtxo(psbt, utxo, keyDerivation.getDerivation().stream().map(ChildNumber::i).toList(),
psbtInput.getRedeemScript(), psbtInput.getWitnessScript()));
inputs.add(new TxInput(psbtInput.getInput().getOutpoint().getHash(), (int)psbtInput.getInput().getOutpoint().getIndex(),
utxo.getValue(), psbtInput.getInput().getSequenceNumber(),
keyDerivation, scriptConfigIndex, prevTx));
}
List<Object> outputs = new ArrayList<>();
for(int i = 0; i < psbt.getPsbtOutputs().size(); i++) {
PSBTOutput psbtOutput = psbt.getPsbtOutputs().get(i);
TransactionOutput txOutput = psbt.getTransaction().getOutputs().get(i);
Map<ECKey, KeyDerivation> derivedPubKeys = new HashMap<>(psbtOutput.getDerivedPublicKeys());
if(psbtOutput.getTapInternalKey() != null) {
Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys = psbtOutput.getTapDerivedPublicKeys();
if(tapDerivedPublicKeys != null) {
for(ECKey ecKey : tapDerivedPublicKeys.keySet()) {
Map<KeyDerivation, List<Sha256Hash>> tapKeyDerivationMap = tapDerivedPublicKeys.get(ecKey);
for(KeyDerivation keyDerivation : tapKeyDerivationMap.keySet()) {
if(!tapKeyDerivationMap.get(keyDerivation).isEmpty()) {
throw new DeviceException("The BitBox02 does not support Taproot script path spending. Found " + tapKeyDerivationMap.size() + " leaf hashes");
}
derivedPubKeys.put(ecKey, keyDerivation);
break;
}
}
}
}
Optional<Map.Entry<ECKey, KeyDerivation>> optOurKey = findOurKey(bitBox02Device, derivedPubKeys);
boolean isChange = optOurKey.isPresent() && optOurKey.get().getValue().getDerivation().get(optOurKey.get().getValue().getDerivation().size() - 2) == KeyPurpose.CHANGE.getPathIndex();
if(isChange) {
KeyDerivation keyDerivation = optOurKey.get().getValue();
int scriptConfigIndex = addScriptConfig(scriptConfigs, getScriptConfigFromUtxo(psbt, txOutput, keyDerivation.getDerivation().stream().map(ChildNumber::i).toList(),
psbtOutput.getRedeemScript(), psbtOutput.getWitnessScript()));
outputs.add(new TxOutputInternal(keyDerivation, txOutput.getValue(), scriptConfigIndex));
} else {
Btc.BTCOutputType type;
byte[] payload;
if(ScriptType.P2PKH.isScriptType(txOutput.getScript())) {
type = Btc.BTCOutputType.P2PKH;
payload = Arrays.copyOfRange(txOutput.getScript().getProgram(), 3,23);
} else if(ScriptType.P2WPKH.isScriptType(txOutput.getScript())) {
type = Btc.BTCOutputType.P2WPKH;
payload = Arrays.copyOfRange(txOutput.getScript().getProgram(), 2, txOutput.getScript().getProgram().length);
} else if(ScriptType.P2SH.isScriptType(txOutput.getScript())) {
type = Btc.BTCOutputType.P2SH;
payload = Arrays.copyOfRange(txOutput.getScript().getProgram(), 2, 22);
} else if(ScriptType.P2WSH.isScriptType(txOutput.getScript())) {
type = Btc.BTCOutputType.P2WSH;
payload = Arrays.copyOfRange(txOutput.getScript().getProgram(), 2, txOutput.getScript().getProgram().length);
} else if(ScriptType.P2TR.isScriptType(txOutput.getScript())) {
type = Btc.BTCOutputType.P2TR;
payload = Arrays.copyOfRange(txOutput.getScript().getProgram(), 2, txOutput.getScript().getProgram().length);
} else {
throw new DeviceException("Output type not recognized for output " + i);
}
outputs.add(new TxOutputExternal(type, payload, txOutput.getValue()));
}
}
if(bip44Account == null) {
throw new DeviceException("No account found");
}
Btc.BTCScriptConfig firstScriptConfig = scriptConfigs.getFirst().getScriptConfig();
if(scriptConfigs.size() == 1 && firstScriptConfig.getConfigCase() == Btc.BTCScriptConfig.ConfigCase.MULTISIG) {
String name = getWalletName(psbt, firstScriptConfig);
registerScriptConfig(bitBox02Device, firstScriptConfig, scriptConfigs.getFirst().getKeypathList(), name == null ? "" : name);
}
List<Signature> sigs = btcSign(bitBox02Device, scriptConfigs, inputs, outputs, (int)psbt.getTransaction().getLocktime(), (int)psbt.getTransaction().getVersion());
for(int i = 0; i < sigs.size(); i++) {
Signature signature = sigs.get(i);
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
ECKey pubKey = foundPubKeys.get(i);
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature.signature, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature.signature, 32, 64));
if(psbtInput.getTapInternalKey() != null) {
psbtInput.setTapKeyPathSignature(new TransactionSignature(r, s, TransactionSignature.Type.SCHNORR));
} else {
psbtInput.getPartialSignatures().put(pubKey, new TransactionSignature(r, s, TransactionSignature.Type.ECDSA));
}
}
return psbt;
}
}
private String getWalletName(PSBT psbt, Btc.BTCScriptConfig firstScriptConfig) {
if(psbt.getExtendedPublicKeys().isEmpty()) {
return null;
}
try {
ScriptType scriptType = switch(firstScriptConfig.getMultisig().getScriptType()) {
case P2WSH -> ScriptType.P2WSH;
case P2WSH_P2SH -> ScriptType.P2SH_P2WSH;
default -> throw new IllegalStateException("Unrecognised multisig script type: " + firstScriptConfig.getMultisig().getScriptType());
};
int m = firstScriptConfig.getMultisig().getThreshold();
OutputDescriptor walletDescriptor = new OutputDescriptor(scriptType, m, psbt.getExtendedPublicKeys());
return getWalletName(walletDescriptor);
} catch(Exception e) {
log.warn("Unable to determine wallet descriptor", e);
}
return null;
}
/**
* coin: the first element of all provided keypaths must match the coin:
* - BTC: 0 + HARDENED
* - Testnets: 1 + HARDENED
* - LTC: 2 + HARDENED
* script_configs: types of all inputs and change outputs. The first element of all provided
* keypaths must match this type:
* - SCRIPT_P2PKH: 44 + HARDENED
* - SCRIPT_P2WPKH_P2SH: 49 + HARDENED
* - SCRIPT_P2WPKH: 84 + HARDENED
* - SCRIPT_P2TR: 86 + HARDENED
* inputs: transaction inputs. The previous transactions of the inputs need to be provided
* if `btc_sign_needs_prevtxs()` returns True.
* outputs: transaction outputs. Can be an external output
* (BTCOutputExternal) or an internal output for change (BTCOutputInternal).
* version, locktime: reserved for future use.
* Returns: list of (input index, signature) tuples.
* Raises Bitbox02Exception with ERR_USER_ABORT on user abort.
*
* @param scriptConfigs script configurations
* @param inputs inputs to sign
* @param outputs outputs to sign
* @param locktime transaction locktime
* @param version transaction version
*
*/
private List<Signature> btcSign(BitBox02Device bitBox02Device, List<Btc.BTCScriptConfigWithKeypath> scriptConfigs, List<TxInput> inputs, List<Object> outputs, int locktime, int version) throws DeviceException {
if(scriptConfigs.stream().anyMatch(this::isTaproot)) {
bitBox02Device.requireAtLeastVersion(new Version("9.10.0"));
}
List<Signature> sigs = new ArrayList<>();
boolean supportsAntiKlepto = (bitBox02Device.getVersion().compareTo(new Version("9.4.0")) >= 0);
Hww.Request.Builder request = Hww.Request.newBuilder();
request.setBtcSignInit(Btc.BTCSignInitRequest.newBuilder()
.setCoin(getCoin())
.addAllScriptConfigs(scriptConfigs)
.setVersion(version)
.setNumInputs(inputs.size())
.setNumOutputs(outputs.size())
.setLocktime(locktime)
.setFormatUnit(Btc.BTCSignInitRequest.FormatUnit.DEFAULT).build());
Btc.BTCSignNextResponse nextResponse = bitBox02Device.msgQuery(request.build(), Hww.Response.ResponseCase.BTC_SIGN_NEXT).getBtcSignNext();
boolean isInputsPass2 = false;
while(true) {
if(nextResponse.getType() == Btc.BTCSignNextResponse.Type.INPUT) {
int inputIndex = nextResponse.getIndex();
TxInput input = inputs.get(inputIndex);
Btc.BTCSignInputRequest.Builder btcSignInputRequest = Btc.BTCSignInputRequest.newBuilder()
.setPrevOutHash(ByteString.copyFrom(serUInt256(input.prevOutHash.toBigInteger())))
.setPrevOutIndex(input.prevOutIndex)
.setPrevOutValue(input.prevOutValue)
.setSequence((int)input.sequence)
.addAllKeypath(input.keyDerivation().getDerivation().stream().map(ChildNumber::i).toList())
.setScriptConfigIndex(input.scriptConfigIndex);
boolean inputIsSchnorr = isTaproot(scriptConfigs.get(input.scriptConfigIndex));
boolean performAntiKlepto = supportsAntiKlepto && isInputsPass2 && !inputIsSchnorr;
ByteString hostNonce = null;
if(performAntiKlepto) {
byte[] nonce = new byte[32];
secureRandom.nextBytes(nonce);
hostNonce = ByteString.copyFrom(nonce);
btcSignInputRequest.setHostNonceCommitment(Antiklepto.AntiKleptoHostNonceCommitment.newBuilder()
.setCommitment(ByteString.copyFrom(antiKleptoHostCommit(hostNonce.toByteArray()))).build());
}
request = Hww.Request.newBuilder();
request.setBtcSignInput(btcSignInputRequest.build());
nextResponse = bitBox02Device.msgQuery(request.build(), Hww.Response.ResponseCase.BTC_SIGN_NEXT).getBtcSignNext();
if(performAntiKlepto) {
if(nextResponse.getType() != Btc.BTCSignNextResponse.Type.HOST_NONCE || !nextResponse.hasAntiKleptoSignerCommitment()) {
throw new DeviceException("Anti klepto response commitment not sent");
}
ByteString signerCommitment = nextResponse.getAntiKleptoSignerCommitment().getCommitment();
Btc.BTCRequest btcRequest = Btc.BTCRequest.newBuilder()
.setAntikleptoSignature(Antiklepto.AntiKleptoSignatureRequest.newBuilder().setHostNonce(hostNonce)).build();
nextResponse = bitBox02Device.btcMsgQuery(btcRequest, Btc.BTCResponse.ResponseCase.SIGN_NEXT).getSignNext();
if(log.isDebugEnabled()) {
log.debug("For input " + inputIndex + ", the host contributed the nonce " + Utils.bytesToHex(hostNonce.toByteArray()));
}
antiKleptoVerify(hostNonce.toByteArray(), signerCommitment.toByteArray(), nextResponse.getSignature().toByteArray());
if(log.isDebugEnabled()) {
log.debug("Antiklepto nonce verification PASSED for input " + inputIndex);
}
}
if(isInputsPass2) {
sigs.add(new Signature(inputIndex, nextResponse.getSignature().toByteArray()));
}
if(inputIndex == inputs.size() - 1) {
isInputsPass2 = true;
}
} else if(nextResponse.getType() == Btc.BTCSignNextResponse.Type.PREVTX_INIT) {
Transaction prevTx = inputs.get(nextResponse.getIndex()).prevTx;
if(prevTx == null) {
throw new DeviceException("Previous transaction missing");
}
Btc.BTCRequest.Builder btcRequest = Btc.BTCRequest.newBuilder()
.setPrevtxInit(Btc.BTCPrevTxInitRequest.newBuilder()
.setVersion((int)prevTx.getVersion())
.setNumInputs(prevTx.getInputs().size())
.setNumOutputs(prevTx.getOutputs().size())
.setLocktime((int)prevTx.getLocktime()).build());
nextResponse = bitBox02Device.btcMsgQuery(btcRequest.build(), Btc.BTCResponse.ResponseCase.SIGN_NEXT).getSignNext();
} else if(nextResponse.getType() == Btc.BTCSignNextResponse.Type.PREVTX_INPUT) {
Transaction prevTx = inputs.get(nextResponse.getIndex()).prevTx;
if(prevTx == null) {
throw new DeviceException("Previous transaction missing");
}
TransactionInput prevTxInput = prevTx.getInputs().get(nextResponse.getPrevIndex());
Btc.BTCRequest.Builder btcRequest = Btc.BTCRequest.newBuilder()
.setPrevtxInput(Btc.BTCPrevTxInputRequest.newBuilder()
.setPrevOutHash(ByteString.copyFrom(serUInt256(prevTxInput.getOutpoint().getHash().toBigInteger())))
.setPrevOutIndex((int)prevTxInput.getOutpoint().getIndex())
.setSignatureScript(ByteString.copyFrom(prevTxInput.getScriptBytes()))
.setSequence((int)prevTxInput.getSequenceNumber()).build());
nextResponse = bitBox02Device.btcMsgQuery(btcRequest.build(), Btc.BTCResponse.ResponseCase.SIGN_NEXT).getSignNext();
} else if(nextResponse.getType() == Btc.BTCSignNextResponse.Type.PREVTX_OUTPUT) {
Transaction prevTx = inputs.get(nextResponse.getIndex()).prevTx;
if(prevTx == null) {
throw new DeviceException("Previous transaction missing");
}
TransactionOutput prevTxOutput = prevTx.getOutputs().get(nextResponse.getPrevIndex());
Btc.BTCRequest.Builder btcRequest = Btc.BTCRequest.newBuilder()
.setPrevtxOutput(Btc.BTCPrevTxOutputRequest.newBuilder()
.setValue(prevTxOutput.getValue())
.setPubkeyScript(ByteString.copyFrom(prevTxOutput.getScriptBytes())).build());
nextResponse = bitBox02Device.btcMsgQuery(btcRequest.build(), Btc.BTCResponse.ResponseCase.SIGN_NEXT).getSignNext();
} else if(nextResponse.getType() == Btc.BTCSignNextResponse.Type.OUTPUT) {
int outputIndex = nextResponse.getIndex();
Object txOutput = outputs.get(outputIndex);
request = Hww.Request.newBuilder();
if(txOutput instanceof TxOutputInternal txOutputInternal) {
request.setBtcSignOutput(Btc.BTCSignOutputRequest.newBuilder()
.setOurs(true)
.setValue(txOutputInternal.value)
.addAllKeypath(txOutputInternal.keyDerivation.getDerivation().stream().map(ChildNumber::i).toList())
.setScriptConfigIndex(txOutputInternal.scriptConfigIndex).build());
} else if(txOutput instanceof TxOutputExternal txOutputExternal) {
request.setBtcSignOutput(Btc.BTCSignOutputRequest.newBuilder()
.setOurs(false)
.setType(txOutputExternal.outputType)
.setPayload(ByteString.copyFrom(txOutputExternal.payload))
.setValue(txOutputExternal.value).build());
}
nextResponse = bitBox02Device.msgQuery(request.build(), Hww.Response.ResponseCase.BTC_SIGN_NEXT).getBtcSignNext();
} else if(nextResponse.getType() == Btc.BTCSignNextResponse.Type.DONE) {
break;
} else {
throw new DeviceException("Unexpected response");
}
}
return sigs;
}
private boolean isTaproot(Btc.BTCScriptConfigWithKeypath scriptConfig) {
return scriptConfig.getScriptConfig().getConfigCase() == Btc.BTCScriptConfig.ConfigCase.SIMPLE_TYPE &&
scriptConfig.getScriptConfig().getSimpleType() == Btc.BTCScriptConfig.SimpleType.P2TR;
}
private byte[] antiKleptoHostCommit(byte[] hostNonce) {
return Utils.taggedHash("s2c/ecdsa/data", hostNonce);
}
/**
* Verifies that hostNonce was used to tweak the nonce during signature
* generation according to k' = k + H(signerCommitment, hostNonce) by checking that
* k'*G = signerCommitment + H(signerCommitment, hostNonce)*G.
* Throws ECDSANonceException if the verification fails.
*
* @param hostNonce the host nonce
* @param signerCommitment signed message
* @param signature the signature
*/
private void antiKleptoVerify(byte[] hostNonce, byte[] signerCommitment, byte[] signature) throws ECDSANonceException {
ECKey signerCommitmentKey = ECKey.fromPublicOnly(signerCommitment);
//Compute R = R1 + H(R1, host_nonce)*G. R1 is the client nonce commitment.
byte[] tweak = Utils.taggedHash("s2c/ecdsa/point", Utils.concat(signerCommitment, hostNonce));
ECKey tweakPubKey = ECKey.fromPrivate(tweak);
ECKey tweakedNonce = tweakPubKey.add(signerCommitmentKey);
BigInteger expectedSigR = tweakedNonce.moduloCurveOrder();
BigInteger actualSigR = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32));
if(!actualSigR.equals(expectedSigR)) {
throw new ECDSANonceException("Could not verify that the host nonce was contributed to the signature. If this happens repeatedly, the device might be attempting to leak the seed through the signature.");
}
}
private Optional<Map.Entry<ECKey, KeyDerivation>> findOurKey(BitBox02Device bitBox02Device, Map<ECKey, KeyDerivation> keyDerivationMap) throws DeviceException {
initializeMasterFingerprint(bitBox02Device);
return keyDerivationMap.entrySet().stream().filter(entry -> entry.getValue().getMasterFingerprint().equals(masterFingerprint)).findFirst();
}
private int addScriptConfig(List<Btc.BTCScriptConfigWithKeypath> scriptConfigs, Btc.BTCScriptConfigWithKeypath scriptConfig) {
for(int i = 0; i < scriptConfigs.size(); i++) {
if(scriptConfigs.get(i).toString().equals(scriptConfig.toString())) {
return i;
}
}
scriptConfigs.add(scriptConfig);
return scriptConfigs.size() - 1;
}
private Btc.BTCScriptConfigWithKeypath getScriptConfigFromUtxo(PSBT psbt, TransactionOutput output, List<Integer> keypath, Script redeemScript, Script witnessScript) throws DeviceException {
if(ScriptType.P2PKH.isScriptType(output.getScript())) {
throw new DeviceException("The BitBox02 does not support legacy p2pkh scripts");
}
if(ScriptType.P2WPKH.isScriptType(output.getScript())) {
return Btc.BTCScriptConfigWithKeypath.newBuilder().setScriptConfig(Btc.BTCScriptConfig.newBuilder()
.setSimpleType(Btc.BTCScriptConfig.SimpleType.P2WPKH)).addAllKeypath(getHardenedPrefix(keypath)).build();
}
if(ScriptType.P2SH_P2WPKH.isScriptType(output.getScript())) {
return Btc.BTCScriptConfigWithKeypath.newBuilder().setScriptConfig(Btc.BTCScriptConfig.newBuilder()
.setSimpleType(Btc.BTCScriptConfig.SimpleType.P2WPKH_P2SH)).addAllKeypath(getHardenedPrefix(keypath)).build();
}
if(ScriptType.P2TR.isScriptType(output.getScript())) {
return Btc.BTCScriptConfigWithKeypath.newBuilder().setScriptConfig(Btc.BTCScriptConfig.newBuilder()
.setSimpleType(Btc.BTCScriptConfig.SimpleType.P2TR)).addAllKeypath(getHardenedPrefix(keypath)).build();
}
if(ScriptType.P2WSH.isScriptType(output.getScript()) || (ScriptType.P2SH.isScriptType(output.getScript()) && ScriptType.P2WSH.isScriptType(redeemScript))) {
if(ScriptType.MULTISIG.isScriptType(witnessScript)) {
int threshold = ScriptType.MULTISIG.getThreshold(witnessScript);
ECKey[] pubKeys = ScriptType.MULTISIG.getPublicKeysFromScript(witnessScript);
/* We assume that all xpubs in the PSBT are part of the multisig. This is okay
since the BitBox02 enforces the same script type for all inputs and
changes. If that should change, we need to find and use the subset of xpubs
corresponding to the public keys in the current multisig script. */
return buildMultisigScriptConfig(threshold, psbt.getExtendedPublicKeys(),
ScriptType.P2WSH.isScriptType(output.getScript()) ? Btc.BTCScriptConfig.Multisig.ScriptType.P2WSH : Btc.BTCScriptConfig.Multisig.ScriptType.P2WSH_P2SH);
}
}
throw new DeviceException("Input or change script type not recognized");
}
private List<Integer> getHardenedPrefix(List<Integer> keypath) {
return keypath.stream().takeWhile(ChildNumber::hasHardenedBit).toList();
}
@Override
String signMessage(String message, String path) throws DeviceException {
List<ChildNumber> fullPath = KeyDerivation.parsePath(path);
Btc.BTCScriptConfig.SimpleType btcScriptConfigType;
if(PURPOSE_P2WPKH.equals(fullPath.get(0))) {
btcScriptConfigType = Btc.BTCScriptConfig.SimpleType.P2WPKH;
} else if(PURPOSE_P2WPKH_P2SH.equals(fullPath.get(0))) {
btcScriptConfigType = Btc.BTCScriptConfig.SimpleType.P2WPKH_P2SH;
} else {
throw new DeviceException("For message signing, the keypath bip44 purpose must be 84' or 49'");
}
if(Network.get() != Network.MAINNET) {
throw new DeviceException("The BitBox02 only supports signing messages on mainnet");
}
Btc.BTCScriptConfigWithKeypath btcScriptConfigWithKeypath = Btc.BTCScriptConfigWithKeypath.newBuilder()
.setScriptConfig(Btc.BTCScriptConfig.newBuilder().setSimpleType(btcScriptConfigType).build())
.addAllKeypath(fullPath.stream().map(ChildNumber::i).toList()).build();
try(BitBox02Device bitBox02Device = new BitBox02Device(hidDevice, new U2FHid(new HidPhysicalLayer(hidDevice)), noiseConfig)) {
byte[] sigBytes = btcSignMsg(bitBox02Device, btcScriptConfigWithKeypath, message);
return Base64.getEncoder().encodeToString(sigBytes);
}
}
private byte[] btcSignMsg(BitBox02Device bitBox02Device, Btc.BTCScriptConfigWithKeypath btcScriptConfigWithKeypath, String message) throws DeviceException {
bitBox02Device.requireAtLeastVersion(new Version("9.2.0"));
Btc.BTCSignMessageRequest.Builder signMessage = Btc.BTCSignMessageRequest.newBuilder()
.setCoin(getCoin())
.setScriptConfig(btcScriptConfigWithKeypath)
.setMsg(ByteString.copyFrom(message, StandardCharsets.UTF_8));
Btc.BTCRequest.Builder btcRequest = Btc.BTCRequest.newBuilder();
ByteString signature;
boolean supportsAntiKlepto = (bitBox02Device.getVersion().compareTo(new Version("9.5.0")) >= 0);
if(supportsAntiKlepto) {
byte[] nonce = new byte[32];
secureRandom.nextBytes(nonce);
signMessage.setHostNonceCommitment(Antiklepto.AntiKleptoHostNonceCommitment.newBuilder()
.setCommitment(ByteString.copyFrom(antiKleptoHostCommit(nonce))).build());
btcRequest.setSignMessage(signMessage.build());
ByteString signerCommitment = bitBox02Device.btcMsgQuery(btcRequest.build(), Btc.BTCResponse.ResponseCase.ANTIKLEPTO_SIGNER_COMMITMENT)
.getAntikleptoSignerCommitment().getCommitment();
btcRequest = Btc.BTCRequest.newBuilder().setAntikleptoSignature(Antiklepto.AntiKleptoSignatureRequest.newBuilder()
.setHostNonce(ByteString.copyFrom(nonce)));
signature = bitBox02Device.btcMsgQuery(btcRequest.build(), Btc.BTCResponse.ResponseCase.SIGN_MESSAGE).getSignMessage().getSignature();
antiKleptoVerify(nonce, signerCommitment.toByteArray(), signature.toByteArray());
if(log.isDebugEnabled()) {
log.debug("Antiklepto nonce verification PASSED");
}
} else {
btcRequest.setSignMessage(signMessage.build());
signature = bitBox02Device.btcMsgQuery(btcRequest.build(), Btc.BTCResponse.ResponseCase.SIGN_MESSAGE).getSignMessage().getSignature();
}
byte[] sigBytes = signature.toByteArray();
ByteBuffer buf = ByteBuffer.allocate(65);
buf.put((byte)(27 + 4 + sigBytes[64]));
buf.put(Arrays.copyOfRange(sigBytes, 0, 64));
return buf.array();
}
@Override
String displaySinglesigAddress(String path, ScriptType scriptType) throws DeviceException {
Btc.BTCScriptConfig scriptConfig = switch(scriptType) {
case P2SH_P2WPKH -> Btc.BTCScriptConfig.newBuilder().setSimpleType(Btc.BTCScriptConfig.SimpleType.P2WPKH_P2SH).build();
case P2WPKH -> Btc.BTCScriptConfig.newBuilder().setSimpleType(Btc.BTCScriptConfig.SimpleType.P2WPKH).build();
case P2TR -> Btc.BTCScriptConfig.newBuilder().setSimpleType(Btc.BTCScriptConfig.SimpleType.P2TR).build();
default -> throw new IllegalArgumentException("The BitBox02 does not support " + scriptType + " addresses");
};
try(BitBox02Device bitBox02Device = new BitBox02Device(hidDevice, new U2FHid(new HidPhysicalLayer(hidDevice)), noiseConfig)) {
return displayAddress(bitBox02Device, scriptConfig, path);
}
}
@Override
String displayMultisigAddress(OutputDescriptor outputDescriptor) throws DeviceException {
try(BitBox02Device bitBox02Device = new BitBox02Device(hidDevice, new U2FHid(new HidPhysicalLayer(hidDevice)), noiseConfig)) {
initializeMasterFingerprint(bitBox02Device);
Optional<ExtendedKey> optOurXpub = outputDescriptor.getExtendedPublicKeys().stream().filter(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint().equals(masterFingerprint)).findFirst();
if(optOurXpub.isEmpty()) {
throw new DeviceException("This BitBox02 is not one of the cosigners");
}
ExtendedKey ourXpub = optOurXpub.get();
Set<List<ChildNumber>> derivationPaths = new HashSet<>(outputDescriptor.getExtendedPublicKeys().stream().map(outputDescriptor::getChildDerivation).toList());
if(derivationPaths.size() > 1) {
throw new IllegalArgumentException("All multisig path suffixes must be the same for the BitBox02");
}
Map<ExtendedKey, KeyDerivation> keyOrigins = new LinkedHashMap<>();
for(ExtendedKey extendedKey : outputDescriptor.sortExtendedPubKeys(outputDescriptor.getExtendedPublicKeys())) {
keyOrigins.put(extendedKey, outputDescriptor.getKeyDerivation(extendedKey));
}
Btc.BTCScriptConfig.Multisig.ScriptType scriptType = switch(outputDescriptor.getScriptType()) {
case P2SH_P2WSH -> Btc.BTCScriptConfig.Multisig.ScriptType.P2WSH_P2SH;
case P2WSH -> Btc.BTCScriptConfig.Multisig.ScriptType.P2WSH;
default -> throw new IllegalArgumentException("The BitBox02 does not support " + outputDescriptor.getScriptType() + " addresses");
};
Btc.BTCScriptConfigWithKeypath btcScriptConfigWithKeypath = buildMultisigScriptConfig(outputDescriptor.getMultisigThreshold(), keyOrigins, scriptType);
String name = getWalletName(outputDescriptor);
registerScriptConfig(bitBox02Device, btcScriptConfigWithKeypath.getScriptConfig(), btcScriptConfigWithKeypath.getKeypathList(), name == null ? "" : name);
return displayAddress(bitBox02Device, btcScriptConfigWithKeypath.getScriptConfig(),
outputDescriptor.getKeyDerivation(ourXpub).extend(KeyDerivation.parsePath(outputDescriptor.getChildDerivationPath(ourXpub))).getDerivationPath());
}
}
private String displayAddress(BitBox02Device bitBox02Device, Btc.BTCScriptConfig scriptConfig, String path) throws DeviceException {
Hww.Request.Builder request = Hww.Request.newBuilder();
request.setBtcPub(Btc.BTCPubRequest.newBuilder().setCoin(getCoin()).addAllKeypath(KeyDerivation.parsePath(path).stream().map(ChildNumber::i).toList())
.setScriptConfig(scriptConfig).setDisplay(true));
Hww.Response hwwResponse = bitBox02Device.msgQuery(request.build(), null);
return hwwResponse.getPub().getPub();
}
private Btc.BTCScriptConfigWithKeypath buildMultisigScriptConfig(int threshold, Map<ExtendedKey, KeyDerivation> keyOrigins, Btc.BTCScriptConfig.Multisig.ScriptType scriptType) {
List<ExtendedKey> keys = new ArrayList<>(keyOrigins.keySet());
int ourXpubIndex = -1;
List<Integer> ourKeyPath = Collections.emptyList();
for(int i = 0; i < keyOrigins.size(); i++) {
ExtendedKey key = keys.get(i);
KeyDerivation keyDerivation = keyOrigins.get(key);
if(keyDerivation.getMasterFingerprint().equals(masterFingerprint)) {
ourXpubIndex = i;
ourKeyPath = keyDerivation.getDerivation().stream().map(ChildNumber::i).toList();
break;
}
}
Btc.BTCScriptConfig.Multisig.Builder builder = Btc.BTCScriptConfig.Multisig.newBuilder()
.setThreshold(threshold)
.setScriptType(scriptType)
.setOurXpubIndex(ourXpubIndex)
.addAllXpubs(keys.stream().map(this::getXpub).toList());
return Btc.BTCScriptConfigWithKeypath.newBuilder().setScriptConfig(Btc.BTCScriptConfig.newBuilder().setMultisig(builder.build())).addAllKeypath(ourKeyPath).build();
}
private void registerScriptConfig(BitBox02Device bitBox02Device, Btc.BTCScriptConfig btcScriptConfig, List<Integer> keypath, String name) throws DeviceException {
boolean isRegistered = isScriptConfigRegistered(bitBox02Device, btcScriptConfig, keypath);
if(!isRegistered) {
if(name.isEmpty()) {
bitBox02Device.requireAtLeastVersion(new Version("9.3.0"));
}
if(name.length() > 30) {
throw new DeviceException("Multisig name is too long, must be 30 characters or less");
}
Btc.BTCScriptConfigRegistration scriptConfigRegistration = Btc.BTCScriptConfigRegistration.newBuilder()
.setCoin(getCoin())
.setScriptConfig(btcScriptConfig)
.addAllKeypath(keypath).build();
Btc.BTCRegisterScriptConfigRequest registerScriptConfigRequest = Btc.BTCRegisterScriptConfigRequest.newBuilder()
.setRegistration(scriptConfigRegistration)
.setName(name)
.setXpubType(Btc.BTCRegisterScriptConfigRequest.XPubType.AUTO_XPUB_TPUB).build();
Btc.BTCRequest request = Btc.BTCRequest.newBuilder().setRegisterScriptConfig(registerScriptConfigRequest).build();
try {
bitBox02Device.btcMsgQuery(request, Btc.BTCResponse.ResponseCase.SUCCESS);
} catch(BitBox02Exception e) {
if(e.getCode() == ERR_DUPLICATE_ENTRY) {
throw new DeviceException("A multisig account configuration with this name already exists. Choose another name.");
}
throw e;
}
}
}
private boolean isScriptConfigRegistered(BitBox02Device bitBox02Device, Btc.BTCScriptConfig btcScriptConfig, List<Integer> keypath) throws DeviceException {
Btc.BTCRequest.Builder request = Btc.BTCRequest.newBuilder();
request.setIsScriptConfigRegistered(Btc.BTCIsScriptConfigRegisteredRequest.newBuilder()
.setRegistration(Btc.BTCScriptConfigRegistration.newBuilder()
.setCoin(getCoin())
.setScriptConfig(btcScriptConfig)
.addAllKeypath(keypath)).build());
Btc.BTCResponse response = bitBox02Device.btcMsgQuery(request.build(), Btc.BTCResponse.ResponseCase.IS_SCRIPT_CONFIG_REGISTERED);
return response.getIsScriptConfigRegistered().getIsRegistered();
}
private Common.XPub getXpub(ExtendedKey xpub) {
return Common.XPub.newBuilder()
.setChainCode(ByteString.copyFrom(xpub.getKey().getChainCode()))
.setPublicKey(ByteString.copyFrom(xpub.getKey().getPubKey()))
.setParentFingerprint(ByteString.copyFrom(xpub.getParentFingerprint()))
.setChildNum(xpub.getKey().getChildNumber().i())
.setDepth(ByteString.copyFrom(ByteBuffer.allocate(1).put((byte)xpub.getKey().getDepth()).array())).build();
}
private Btc.BTCCoin getCoin() {
return Network.get() == Network.MAINNET ? Btc.BTCCoin.BTC : Btc.BTCCoin.TBTC;
}
@Override
public boolean togglePassphrase() throws DeviceException {
try(BitBox02Device bitBox02Device = new BitBox02Device(hidDevice, new U2FHid(new HidPhysicalLayer(hidDevice)), noiseConfig)) {
bitBox02Device.togglePassphrase();
return true;
}
}
@Override
public String getPath() {
return hidDevice.getPath();
}
@Override
public HardwareType getHardwareType() {
return HardwareType.BITBOX_02;
}
@Override
public WalletModel getModel() {
return WalletModel.BITBOX_02;
}
@Override
public String getProductModel() {
return getType() + "_" + edition.getName();
}
@Override
public Boolean needsPinSent() {
return false;
}
@Override
public Boolean needsPassphraseSent() {
return false;
}
@Override
public String fingerprint() {
return masterFingerprint;
}
@Override
public boolean card() {
return false;
}
@Override
public String[][] warnings() {
return new String[0][];
}
public void setNoiseConfig(BitBoxNoiseConfig noiseConfig) {
this.noiseConfig = noiseConfig;
}
private record TxInput(Sha256Hash prevOutHash, int prevOutIndex, long prevOutValue, long sequence, KeyDerivation keyDerivation, int scriptConfigIndex, Transaction prevTx) {}
private record TxOutputExternal(Btc.BTCOutputType outputType, byte[] payload, long value) {}
private record TxOutputInternal(KeyDerivation keyDerivation, long value, int scriptConfigIndex) {}
private record Signature(int index, byte[] signature) {}
}

View File

@ -0,0 +1,17 @@
package com.sparrowwallet.lark;
import com.sparrowwallet.drongo.Network;
public enum Chain {
main(Network.MAINNET), test(Network.TESTNET), regtest(Network.REGTEST), signet(Network.SIGNET);
private final Network network;
Chain(Network network) {
this.network = network;
}
public Network getNetwork() {
return network;
}
}

View File

@ -0,0 +1,277 @@
package com.sparrowwallet.lark;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.coldcard.*;
import org.hid4java.HidDevice;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.lark.coldcard.ColdcardDevice.CKCC_PID;
import static com.sparrowwallet.lark.coldcard.ColdcardDevice.COINKITE_VID;
import static com.sparrowwallet.lark.coldcard.Constants.MAX_BLK_LEN;
public class ColdcardClient extends HardwareClient {
private static final DeviceId COINKITE_ID = new DeviceId(COINKITE_VID, CKCC_PID);
private final HidDevice hidDevice;
private String masterFingerprint;
public ColdcardClient(HidDevice hidDevice) throws DeviceException {
if(COINKITE_ID.matches(hidDevice) && hidDevice.getSerialNumber() != null) {
this.hidDevice = hidDevice;
} else {
throw new DeviceException("Not a Coldcard");
}
}
@Override
void initializeMasterFingerprint() throws DeviceException {
try(ColdcardDevice coldcardDevice = new ColdcardDevice(hidDevice)) {
this.masterFingerprint = Utils.bytesToHex(coldcardDevice.getDeviceId().masterFingerprint());
}
}
@Override
ExtendedKey getPubKeyAtPath(String path) throws DeviceException {
try(ColdcardDevice coldcardDevice = new ColdcardDevice(hidDevice)) {
coldcardDevice.checkMitm();
String rewrittenPath = path.replaceAll("[hH]", "'");
String resp = (String)coldcardDevice.sendRecv(ProtocolPacker.getXpub(rewrittenPath));
return ExtendedKey.fromDescriptor(resp);
}
}
@Override
PSBT signTransaction(PSBT psbt) throws DeviceException {
try(ColdcardDevice coldcardDevice = new ColdcardDevice(hidDevice)) {
coldcardDevice.checkMitm();
String resp = (String)coldcardDevice.sendRecv(ProtocolPacker.getXpub("m/0'"));
ExtendedKey masterXpub = ExtendedKey.fromDescriptor(resp);
String masterFingerprint = Utils.bytesToHex(masterXpub.getParentFingerprint());
int passes = 1;
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
int ourKeys = 0;
for(Map.Entry<ECKey, KeyDerivation> entry : psbtInput.getDerivedPublicKeys().entrySet()) {
if(entry.getValue().getMasterFingerprint().equals(masterFingerprint) && !psbtInput.getPartialSignatures().containsKey(entry.getKey())) {
ourKeys++;
}
if(ourKeys > passes) {
passes = ourKeys;
}
}
}
PSBT signedPsbt = psbt;
for(int i = 0; i < passes; i++) {
byte[] psbtBytes = signedPsbt.serialize();
ByteArrayInputStream bais = new ByteArrayInputStream(psbtBytes);
int size = psbtBytes.length;
int left = size;
MessageDigest digest = Sha256Hash.newDigest();
for(int pos = 0; pos < size; pos+= MAX_BLK_LEN) {
byte[] here = new byte[Math.min(MAX_BLK_LEN, left)];
bais.read(here, 0, here.length);
left -= here.length;
Long uploaded = (Long)coldcardDevice.sendRecv(ProtocolPacker.upload(pos, size, here));
if(uploaded != pos) {
throw new DeviceException("Upload failed, position " + pos + " != " + uploaded);
}
digest.update(here);
}
Sha256Hash calculated = Sha256Hash.wrap(digest.digest());
Sha256Hash received = Sha256Hash.wrap((byte[])coldcardDevice.sendRecv(ProtocolPacker.sha256()));
if(!calculated.equals(received)) {
throw new DeviceException("Wrong checksum, expected " + calculated + " but got " + received);
}
Object decoded = coldcardDevice.sendRecv(ProtocolPacker.signTransaction(size, calculated.getBytes(), false));
if(decoded != null) {
throw new DeviceException("Received unexpected response of " + decoded);
}
Object signResp = null;
while(signResp == null) {
try {
Thread.sleep(250);
} catch(InterruptedException e) {
//ignore
}
signResp = coldcardDevice.sendRecv(ProtocolPacker.getSignedTxn(), true, -1);
}
if(signResp instanceof SignedTransaction signedTx) {
byte[] signedBytes = coldcardDevice.downloadFile(signedTx.length(), signedTx.sha256());
try {
signedPsbt = new PSBT(signedBytes, false);
} catch(PSBTParseException e) {
throw new DeviceException("Invalid signed PSBT", e);
}
} else {
throw new DeviceFailedException("Failed: " + signResp);
}
}
return signedPsbt;
} catch(DeviceException e) {
throw e;
} catch(Exception e) {
throw new DeviceException("Failed to sign transaction", e);
}
}
@Override
String signMessage(String message, String path) throws DeviceException {
try(ColdcardDevice coldcardDevice = new ColdcardDevice(hidDevice)) {
coldcardDevice.checkMitm();
String rewrittenPath = path.replaceAll("[hH]", "'");
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
Object resp = coldcardDevice.sendRecv(ProtocolPacker.signMessage(msgBytes, rewrittenPath), true, -1);
if(resp != null) {
throw new DeviceException("Received unexpected response of " + resp);
}
Object result = null;
while(result == null) {
try {
Thread.sleep(250);
} catch(InterruptedException e) {
//ignore
}
result = coldcardDevice.sendRecv(ProtocolPacker.getSignedMessage(), true, -1);
}
if(result instanceof SignedMessage signedMessage) {
return new String(Base64.getEncoder().encode(signedMessage.signature()), StandardCharsets.US_ASCII);
} else {
throw new DeviceFailedException("Failed: " + result);
}
}
}
@Override
String displaySinglesigAddress(String path, ScriptType scriptType) throws DeviceException {
try(ColdcardDevice coldcardDevice = new ColdcardDevice(hidDevice)) {
coldcardDevice.checkMitm();
String rewrittenPath = path.replaceAll("[hH]", "'");
int addressFormat = getAddressFormat(scriptType);
return (String)coldcardDevice.sendRecv(ProtocolPacker.showAddress(rewrittenPath, addressFormat));
}
}
@Override
String displayMultisigAddress(OutputDescriptor outputDescriptor) throws DeviceException {
if(!outputDescriptor.isMultisig()) {
throw new IllegalArgumentException("Output descriptor is not a multisig descriptor: " + outputDescriptor);
}
try(ColdcardDevice coldcardDevice = new ColdcardDevice(hidDevice)) {
coldcardDevice.checkMitm();
int addressFormat = getAddressFormat(outputDescriptor.getScriptType());
if(outputDescriptor.getExtendedPublicKeys().isEmpty() || outputDescriptor.getExtendedPublicKeys().size() > 15) {
throw new IllegalArgumentException("Must provide 1 to 15 keypaths to display a multisig address");
}
if(outputDescriptor.getMultisigThreshold() < 1 || outputDescriptor.getMultisigThreshold() > outputDescriptor.getExtendedPublicKeys().size()) {
throw new IllegalArgumentException("Either the redeem script provided is invalid or the keypaths provided are insufficient");
}
List<long[]> xfpPaths = new ArrayList<>();
for(ExtendedKey extendedKey : outputDescriptor.sortExtendedPubKeys(outputDescriptor.getExtendedPublicKeys())) {
KeyDerivation keyDerivation = outputDescriptor.getKeyDerivation(extendedKey);
long[] keyPath = keyDerivation.extend(KeyDerivation.parsePath(outputDescriptor.getChildDerivationPath(extendedKey)))
.getDerivation().stream().mapToLong(num -> Integer.toUnsignedLong(num.i())).toArray();
long[] xfpPath = new long[keyPath.length+1];
System.arraycopy(keyPath, 0, xfpPath, 1, keyPath.length);
byte[] mfp = Utils.hexToBytes(keyDerivation.getMasterFingerprint());
xfpPath[0] = ((long) mfp[0] & 0xFF) | (((long) mfp[1] & 0xFF) << 8) | (((long) mfp[2] & 0xFF) << 16) | (((long) mfp[3] & 0xFF) << 24);;
xfpPaths.add(xfpPath);
}
Collection<ECKey> keys = outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> extKey.getKey(outputDescriptor.getChildDerivation(extKey))).collect(Collectors.toList());
Script redeemScript = ScriptType.MULTISIG.getOutputScript(outputDescriptor.getMultisigThreshold(), keys);
byte[] msg = ProtocolPacker.showP2SHAddress((byte)outputDescriptor.getMultisigThreshold(), xfpPaths, redeemScript.getProgram(), addressFormat);
return (String)coldcardDevice.sendRecv(msg);
}
}
private int getAddressFormat(ScriptType scriptType) throws DeviceException {
return switch(scriptType) {
case P2SH_P2WPKH -> Protocol.AF_P2WPKH_P2SH;
case P2WPKH -> Protocol.AF_P2WPKH;
case P2PKH -> Protocol.AF_CLASSIC;
case P2TR -> throw new DeviceException("Coldcard does not support displaying Taproot addresses yet");
case P2SH_P2WSH -> Protocol.AF_P2WSH_P2SH;
case P2WSH -> Protocol.AF_P2WSH;
case P2SH -> Protocol.AF_P2SH;
default -> throw new DeviceException("Unsupported script type of " + scriptType);
};
}
@Override
public String getPath() {
return hidDevice.getPath();
}
@Override
public HardwareType getHardwareType() {
return HardwareType.COLDCARD;
}
@Override
public WalletModel getModel() {
return WalletModel.COLDCARD;
}
@Override
public Boolean needsPinSent() {
return Boolean.FALSE;
}
@Override
public Boolean needsPassphraseSent() {
return Boolean.FALSE;
}
@Override
public String fingerprint() {
return masterFingerprint;
}
@Override
public boolean card() {
return false;
}
@Override
public String[][] warnings() {
return new String[0][];
}
}

View File

@ -0,0 +1,7 @@
package com.sparrowwallet.lark;
public class DeviceBusyException extends DeviceException {
public DeviceBusyException() {
super("Device is busy");
}
}

View File

@ -0,0 +1,11 @@
package com.sparrowwallet.lark;
public class DeviceException extends Exception {
public DeviceException(String message) {
super(message);
}
public DeviceException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,11 @@
package com.sparrowwallet.lark;
public class DeviceFailedException extends DeviceException {
public DeviceFailedException(String message) {
super(message);
}
public DeviceFailedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,7 @@
package com.sparrowwallet.lark;
public class DeviceFramingException extends DeviceProtocolException {
public DeviceFramingException(String message) {
super(message);
}
}

View File

@ -0,0 +1,43 @@
package com.sparrowwallet.lark;
import com.fazecast.jSerialComm.SerialPort;
import org.hid4java.HidDevice;
import org.usb4java.DeviceDescriptor;
public class DeviceId {
private final int vendorId;
private final int productId;
public DeviceId(int vendorId, int productId) {
this.vendorId = vendorId;
this.productId = productId;
}
public int getVendorId() {
return vendorId;
}
public int getProductId() {
return productId;
}
public boolean matches(HidDevice hidDevice) {
return hidDevice.getVendorId() == vendorId && hidDevice.getProductId() == productId;
}
public boolean matches(SerialPort serialPort) {
return serialPort.getVendorID() == vendorId && serialPort.getProductID() == productId;
}
public boolean matches(DeviceDescriptor deviceDescriptor) {
return deviceDescriptor.idVendor() == vendorId && deviceDescriptor.idProduct() == productId;
}
@Override
public String toString() {
return "DeviceId{" +
"vendorId=" + getVendorId() +
", productId=" + getProductId() +
'}';
}
}

View File

@ -0,0 +1,11 @@
package com.sparrowwallet.lark;
public class DeviceInitializationException extends DeviceException {
public DeviceInitializationException(String message) {
super(message);
}
public DeviceInitializationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,11 @@
package com.sparrowwallet.lark;
public class DeviceMitmFailedException extends DeviceException {
public DeviceMitmFailedException(String message) {
super(message);
}
public DeviceMitmFailedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,7 @@
package com.sparrowwallet.lark;
public class DeviceNotFoundException extends DeviceException {
public DeviceNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package com.sparrowwallet.lark;
public class DeviceNotReadyException extends DeviceException {
public DeviceNotReadyException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package com.sparrowwallet.lark;
public class DeviceProtocolException extends DeviceException {
public DeviceProtocolException(String message) {
super(message);
}
}

View File

@ -0,0 +1,154 @@
package com.sparrowwallet.lark;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
public abstract class HardwareClient {
protected String error;
private Map<OutputDescriptor, String> walletNames = new HashMap<>();
abstract void initializeMasterFingerprint() throws DeviceException;
abstract ExtendedKey getPubKeyAtPath(String path) throws DeviceException;
abstract PSBT signTransaction(PSBT psbt) throws DeviceException;
abstract String signMessage(String message, String path) throws DeviceException;
abstract String displaySinglesigAddress(String path, ScriptType scriptType) throws DeviceException;
abstract String displayMultisigAddress(OutputDescriptor outputDescriptor) throws DeviceException;
public String getType() {
return getHardwareType().getName();
}
public abstract String getPath();
public abstract HardwareType getHardwareType();
public abstract WalletModel getModel();
public abstract Boolean needsPinSent();
public abstract Boolean needsPassphraseSent();
public abstract String fingerprint();
public abstract boolean card();
public abstract String[][] warnings();
public String error() {
return error;
}
void setError(String error) {
this.error = error;
}
public boolean promptPin() throws DeviceException {
throw new DeviceException("The " + getHardwareType().getDisplayName() + " does not need a PIN sent from the host");
}
public boolean sendPin(String pin) throws DeviceException {
throw new DeviceException("The " + getHardwareType().getDisplayName() + " does not need a PIN sent from the host");
}
public boolean togglePassphrase() throws DeviceException {
throw new DeviceException("The " + getHardwareType().getDisplayName() + " does not support toggling passphrase from the host");
}
public String getLabel() {
return null;
}
public String getProductModel() {
return getType();
}
@Override
public String toString() {
return "{type=\"" + getType() +
"\", model=\"" + getModel() +
"\", path=\"" + getPath() +
"\", fingerprint=\"" + fingerprint() +
"\", needsPinSent=\"" + needsPinSent() +
"\", needsPassphraseSent=\"" + needsPassphraseSent() +
"\", warnings=\"" + Arrays.deepToString(warnings()) +
"\", error=\"" + error() + "\"}";
}
public void setWalletNames(Map<OutputDescriptor, String> walletNames) {
this.walletNames = walletNames;
}
protected String getWalletNameOrDefault(OutputDescriptor outputDescriptor) {
return getWalletNameOrDefault(outputDescriptor, null);
}
protected String getWalletNameOrDefault(OutputDescriptor outputDescriptor, PSBT psbt) {
String name = getWalletName(outputDescriptor);
if(name == null) {
if(psbt != null) {
name = getWalletName(psbt);
}
if(name == null) {
if(outputDescriptor.isMultisig()) {
name = outputDescriptor.getMultisigThreshold() + " of " + outputDescriptor.getExtendedPublicKeys().size() + " Multisig";
} else {
name = "Singlesig";
}
}
}
return name;
}
private String getWalletName(PSBT psbt) {
Optional<Script> optSigningScript = psbt.getPsbtInputs().stream().filter(psbtInput -> psbtInput.getUtxo() != null).map(PSBTInput::getSigningScript).findFirst();
if(optSigningScript.isPresent()) {
if(ScriptType.MULTISIG.isScriptType(optSigningScript.get())) {
int threshold = ScriptType.MULTISIG.getThreshold(optSigningScript.get());
int keys = ScriptType.MULTISIG.getPublicKeysFromScript(optSigningScript.get()).length;
return threshold + " of " + keys + " Multisig";
} else {
return "Singlesig";
}
}
return null;
}
protected String getWalletName(OutputDescriptor walletDescriptor) {
return walletNames.get(walletDescriptor.copy(false));
}
/**
* Serialize a 256-bit integer with Bitcoin's 256-bit integer serialization.
*
* @param u The 256-bit integer as a BigInteger
* @return The serialized 256-bit integer as a byte array
*/
public static byte[] serUInt256(BigInteger u) {
ByteBuffer buffer = ByteBuffer.allocate(32);
buffer.order(ByteOrder.LITTLE_ENDIAN);
// Write each 32-bit segment from the BigInteger to the buffer
for (int i = 0; i < 8; i++) {
int segment = u.and(BigInteger.valueOf(0xFFFFFFFFL)).intValue();
buffer.putInt(segment);
u = u.shiftRight(32);
}
return buffer.array();
}
public static Optional<ScriptType> isWitness(Script inputScript) {
return Stream.of(ScriptType.P2WPKH, ScriptType.P2WSH, ScriptType.P2TR)
.filter(scriptType -> scriptType.isScriptType(inputScript)).findFirst();
}
}

View File

@ -0,0 +1,107 @@
package com.sparrowwallet.lark;
import com.fazecast.jSerialComm.SerialPort;
import org.hid4java.HidDevice;
import org.usb4java.Device;
import org.usb4java.DeviceDescriptor;
public enum HardwareType {
COLDCARD("coldcard") {
@Override
public HardwareClient createClient(HidDevice hidDevice) throws DeviceException {
return new ColdcardClient(hidDevice);
}
},
JADE("jade") {
@Override
public HardwareClient createClient(SerialPort serialPort) throws DeviceException {
return new JadeClient(serialPort);
}
},
BITBOX_02("bitbox02") {
@Override
public HardwareClient createClient(HidDevice hidDevice) throws DeviceException {
return new BitBox02Client(hidDevice);
}
},
TREZOR("trezor") {
@Override
public HardwareClient createClient(Device device, DeviceDescriptor deviceDescriptor) throws DeviceException {
return new TrezorClient(device, deviceDescriptor);
}
},
KEEPKEY("keepkey") {
@Override
public HardwareClient createClient(Device device, DeviceDescriptor deviceDescriptor) throws DeviceException {
return new KeepkeyClient(device, deviceDescriptor);
}
},
LEDGER("ledger") {
@Override
public HardwareClient createClient(HidDevice hidDevice) throws DeviceException {
return new LedgerClient(hidDevice);
}
};
private final String name;
HardwareType(String name) {
this.name = name;
}
public String getName() {
return name;
}
public String getDisplayName() {
return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
}
public HardwareClient createClient(HidDevice hidDevice) throws DeviceException {
throw new DeviceException("Not an HID hardware type");
}
public HardwareClient createClient(SerialPort serialPort) throws DeviceException {
throw new DeviceException("Not a serial hardware type");
}
public HardwareClient createClient(Device device, DeviceDescriptor descriptor) throws DeviceException {
throw new DeviceException("Not a WebUSB hardware type");
}
public static HardwareClient fromHidDevice(HidDevice hidDevice) throws DeviceException {
for(HardwareType type : values()) {
try {
return type.createClient(hidDevice);
} catch(DeviceException e) {
//ignore
}
}
throw new DeviceNotFoundException("No HID hardware type for vendor id: " + hidDevice.getVendorId() + ", product id: " + hidDevice.getProductId());
}
public static HardwareClient fromSerialPort(SerialPort serialPort) throws DeviceException {
for(HardwareType type : values()) {
try {
return type.createClient(serialPort);
} catch(DeviceException e) {
//ignore
}
}
throw new DeviceNotFoundException("No serial hardware type for vendor id: " + serialPort.getVendorID() + ", product id: " + serialPort.getProductID());
}
public static HardwareClient fromWebusbDevice(Device device, DeviceDescriptor deviceDescriptor) throws DeviceException {
for(HardwareType type : values()) {
try {
return type.createClient(device, deviceDescriptor);
} catch(DeviceException e) {
//ignore
}
}
throw new DeviceNotFoundException("No WebUSB type for vendor id: " + deviceDescriptor.idVendor() + ", product id: " + deviceDescriptor.idProduct());
}
}

View File

@ -0,0 +1,158 @@
package com.sparrowwallet.lark;
import com.fazecast.jSerialComm.SerialPort;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.jade.JadeDevice;
import com.sparrowwallet.lark.jade.JadeVersion;
import java.util.List;
public class JadeClient extends HardwareClient {
public static final List<DeviceId> JADE_DEVICE_IDS = List.of(new DeviceId(0x10c4, 0xea60),
new DeviceId(0x1a86, 0x55d4), new DeviceId(0x0403, 0x6001), new DeviceId(0x1a86, 0x7523),
new DeviceId(0x303a, 0x4001), new DeviceId(0x303a, 0x1001));
private static final Version MIN_SUPPORTED_VERSION = new Version("0.1.47");
private final SerialPort serialPort;
private String masterFingerprint;
public JadeClient(SerialPort serialPort) throws DeviceException {
if(JADE_DEVICE_IDS.stream().anyMatch(deviceId -> deviceId.matches(serialPort))) {
this.serialPort = serialPort;
} else {
throw new DeviceException("Not a Jade");
}
}
@Override
void initializeMasterFingerprint() throws DeviceException {
try(JadeDevice jadeDevice = new JadeDevice(serialPort)) {
initialize(jadeDevice);
this.masterFingerprint = Utils.bytesToHex(jadeDevice.getXpub(Network.get(), "m/0h").getParentFingerprint());
}
}
@Override
ExtendedKey getPubKeyAtPath(String path) throws DeviceException {
try(JadeDevice jadeDevice = new JadeDevice(serialPort)) {
initialize(jadeDevice);
return jadeDevice.getXpub(Network.get(), path);
}
}
@Override
PSBT signTransaction(PSBT psbt) throws DeviceException {
try(JadeDevice jadeDevice = new JadeDevice(serialPort)) {
initialize(jadeDevice);
byte[] psbtBytes = psbt.serialize();
byte[] signedPsbtBytes = jadeDevice.signTransaction(Network.get(), psbtBytes);
return new PSBT(signedPsbtBytes);
} catch(PSBTParseException e) {
throw new DeviceException("Invalid signed PSBT", e);
}
}
@Override
String signMessage(String message, String path) throws DeviceException {
try(JadeDevice jadeDevice = new JadeDevice(serialPort)) {
initialize(jadeDevice);
return jadeDevice.signMessage(message, path);
}
}
@Override
String displaySinglesigAddress(String path, ScriptType scriptType) throws DeviceException {
try(JadeDevice jadeDevice = new JadeDevice(serialPort)) {
initialize(jadeDevice);
return jadeDevice.displaySinglesigAddress(Network.get(), path, scriptType);
}
}
@Override
String displayMultisigAddress(OutputDescriptor outputDescriptor) throws DeviceException {
try(JadeDevice jadeDevice = new JadeDevice(serialPort)) {
initialize(jadeDevice);
String name = getWalletNameOrDefault(outputDescriptor);
jadeDevice.registerMultisig(Network.get(), name, outputDescriptor);
return jadeDevice.displayMultisigAddress(Network.get(), name, outputDescriptor);
}
}
private void initialize(JadeDevice jadeDevice) throws DeviceException {
JadeVersion jadeVersion = jadeDevice.getVersionInfo();
if(jadeVersion.JADE_VERSION().compareTo(MIN_SUPPORTED_VERSION) < 0) {
throw new DeviceException("Jade fw version: " + jadeVersion.JADE_VERSION() + " < minimum required version: " + MIN_SUPPORTED_VERSION);
}
jadeDevice.addEntropy();
boolean authenticated = false;
while(!authenticated) {
authenticated = jadeDevice.authUser(Network.get());
}
}
@Override
public String getPath() {
return serialPort.getSystemPortPath();
}
@Override
public HardwareType getHardwareType() {
return HardwareType.JADE;
}
@Override
public WalletModel getModel() {
return WalletModel.JADE;
}
@Override
public Boolean needsPinSent() {
return null;
}
@Override
public Boolean needsPassphraseSent() {
return null;
}
@Override
public String fingerprint() {
return masterFingerprint;
}
@Override
public boolean card() {
return false;
}
@Override
public String[][] warnings() {
return new String[0][];
}
@Override
public final boolean equals(Object o) {
if(this == o) {
return true;
}
if(!(o instanceof JadeClient that)) {
return false;
}
return getModel().equals(that.getModel());
}
@Override
public int hashCode() {
return getModel().hashCode();
}
}

View File

@ -0,0 +1,31 @@
package com.sparrowwallet.lark;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.trezor.TrezorDevice;
import org.usb4java.Device;
import org.usb4java.DeviceDescriptor;
import java.util.List;
public class KeepkeyClient extends TrezorClient {
public static final List<DeviceId> KEEPKEY_DEVICE_IDS = List.of(new DeviceId(0x2B24, 0x0002));
public KeepkeyClient(Device device, DeviceDescriptor deviceDescriptor) throws DeviceException {
super(KEEPKEY_DEVICE_IDS, device, deviceDescriptor);
}
@Override
public HardwareType getHardwareType() {
return HardwareType.KEEPKEY;
}
@Override
public WalletModel getModel() {
return WalletModel.KEEPKEY;
}
@Override
public boolean canSignTaproot(TrezorDevice trezorDevice) {
return false;
}
}

View File

@ -0,0 +1,887 @@
package com.sparrowwallet.lark;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fazecast.jSerialComm.SerialPort;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.args.*;
import com.sparrowwallet.lark.bitbox02.BitBoxNoiseConfig;
import com.sparrowwallet.tern.http.client.HttpClientService;
import org.hid4java.HidDevice;
import org.hid4java.HidManager;
import org.hid4java.HidServices;
import org.hid4java.HidServicesSpecification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import org.usb4java.Device;
import org.usb4java.DeviceDescriptor;
import org.usb4java.DeviceList;
import org.usb4java.LibUsb;
import java.util.*;
/**
* The main interface to the library.
*/
public class Lark {
public static final String APP_NAME = "Lark";
public static final Version APP_VERSION = new Version("0.9");
private static final Logger log = LoggerFactory.getLogger(Lark.class);
private static final HttpClientService httpClientService = new HttpClientService(null);
private static final Object lock = new Object();
private static boolean consoleOutput;
private String passphrase;
private BitBoxNoiseConfig bitBoxNoiseConfig;
private final Map<OutputDescriptor, String> walletNames = new HashMap<>();
private final Map<OutputDescriptor, byte[]> walletRegistrations = new HashMap<>();
static {
LibUsb.init(null);
Runtime.getRuntime().addShutdownHook(new Thread(() -> LibUsb.exit(null)));
}
/**
* Retrieves a list of all connected devices with the given hardware type.
* The master fingerprints will be initialized.
*
* @param hardwareType the type of the devices to filter on
* @return a list of all connected devices with the given type
*/
public List<HardwareClient> enumerate(HardwareType hardwareType) {
return enumerate().stream().filter(device -> device.getHardwareType() == hardwareType).toList();
}
/**
* Retrieves a list of all connected devices with the given model.
* The master fingerprints will be initialized.
*
* @param walletModel the model of the devices to filter on
* @return a list of all connected devices with the given model
*/
public List<HardwareClient> enumerate(WalletModel walletModel) {
return enumerate().stream().filter(device -> device.getModel() == walletModel).toList();
}
/**
* Retrieves a list of all connected devices.
* The master fingerprints will be initialized.
*
* @return a list of all connected devices
*/
public List<HardwareClient> enumerate() {
return enumerate(true);
}
private List<HardwareClient> enumerate(boolean initializeMasterFingerprint) {
synchronized(lock) {
List<HardwareClient> foundClients = new ArrayList<>();
foundClients.addAll(enumerateHidClients(initializeMasterFingerprint));
foundClients.addAll(enumerateSerialClients(initializeMasterFingerprint));
foundClients.addAll(enumerateWebusbClients(initializeMasterFingerprint));
return foundClients;
}
}
private Collection<HardwareClient> enumerateHidClients(boolean initializeMasterFingerprint) {
HidServicesSpecification hidServicesSpecification = new HidServicesSpecification();
hidServicesSpecification.setAutoStart(false);
HidServices hidServices = HidManager.getHidServices(hidServicesSpecification);
Set<HardwareClient> foundClients = new LinkedHashSet<>();
for(HidDevice hidDevice : hidServices.getAttachedHidDevices()) {
HardwareClient hardwareClient = null;
try {
hardwareClient = HardwareType.fromHidDevice(hidDevice);
hardwareClient.setWalletNames(walletNames);
if(hardwareClient instanceof BitBox02Client bitBox02Client && bitBoxNoiseConfig != null) {
bitBox02Client.setNoiseConfig(bitBoxNoiseConfig);
}
if(hardwareClient instanceof LedgerClient ledgerClient) {
ledgerClient.setWalletRegistrations(walletRegistrations);
}
if(foundClients.add(hardwareClient) && initializeMasterFingerprint) {
hardwareClient.initializeMasterFingerprint();
}
} catch(DeviceNotFoundException e) {
//ignore, hid device does not match available hardware types
} catch(DeviceException e) {
if(hardwareClient != null) {
hardwareClient.setError("Could not open client or get fingerprint information: " + e.getMessage());
} else {
log.error("Error initialising hardware client", e);
}
}
}
return foundClients;
}
private Collection<HardwareClient> enumerateSerialClients(boolean initializeMasterFingerprint) {
Set<HardwareClient> foundClients = new LinkedHashSet<>();
SerialPort[] serialPorts = SerialPort.getCommPorts();
for(SerialPort serialPort : serialPorts) {
HardwareClient hardwareClient = null;
try {
hardwareClient = HardwareType.fromSerialPort(serialPort);
hardwareClient.setWalletNames(walletNames);
if(foundClients.add(hardwareClient) && initializeMasterFingerprint) {
hardwareClient.initializeMasterFingerprint();
}
} catch(DeviceNotFoundException e) {
//ignore, serial device does not match available hardware types
} catch(DeviceException e) {
if(hardwareClient != null) {
hardwareClient.setError("Could not open client or get fingerprint information: " + e.getMessage());
} else {
log.error("Error initialising hardware client", e);
}
}
}
return foundClients;
}
private Collection<HardwareClient> enumerateWebusbClients(boolean initializeMasterFingerprint) {
Set<HardwareClient> foundClients = new LinkedHashSet<>();
DeviceList webUsbDevices = new DeviceList();
int result = LibUsb.getDeviceList(null, webUsbDevices);
if(result < 0) {
log.error("Unable to list webusb devices, operation returned " + result);
}
try {
for(Device device : webUsbDevices) {
HardwareClient hardwareClient = null;
try {
DeviceDescriptor descriptor = new DeviceDescriptor();
result = LibUsb.getDeviceDescriptor(device, descriptor);
if(result != LibUsb.SUCCESS) {
continue;
}
descriptor.iProduct();
hardwareClient = HardwareType.fromWebusbDevice(device, descriptor);
hardwareClient.setWalletNames(walletNames);
if(hardwareClient instanceof TrezorClient trezorClient && passphrase != null) {
trezorClient.setPassphrase(passphrase);
}
if(foundClients.add(hardwareClient) && initializeMasterFingerprint) {
hardwareClient.initializeMasterFingerprint();
}
} catch(DeviceNotFoundException e) {
//ignore, serial device does not match available hardware types
} catch(DeviceException e) {
if(hardwareClient != null) {
hardwareClient.setError("Could not open client or get fingerprint information: " + e.getMessage());
} else {
log.error("Error initialising hardware client", e);
}
}
}
} finally {
LibUsb.freeDeviceList(webUsbDevices, true);
}
return foundClients;
}
private HardwareClient getHardwareClient(String deviceType) throws DeviceNotFoundException {
List<HardwareClient> clients = enumerate(false);
for(HardwareClient client : clients) {
if(client.getType().equals(deviceType)) {
return client;
}
}
throw new DeviceNotFoundException("Could not find hardware client with type " + deviceType);
}
private HardwareClient getHardwareClient(String deviceType, String devicePath) throws DeviceNotFoundException {
List<HardwareClient> clients = enumerate(false);
for(HardwareClient client : clients) {
if(client.getType().equals(deviceType) && client.getPath().equals(devicePath)) {
return client;
}
}
throw new DeviceNotFoundException("Could not find hardware client with type " + deviceType + " at path " + devicePath);
}
private HardwareClient getHardwareClient(byte[] fingerprint) throws DeviceNotFoundException {
List<HardwareClient> clients = enumerate(true);
for(HardwareClient client : clients) {
if(client.fingerprint().equals(Utils.bytesToHex(fingerprint))) {
return client;
}
}
throw new DeviceNotFoundException("Could not find hardware client with fingerprint " + Utils.bytesToHex(fingerprint));
}
/**
* Retrieves the xpub at the given path.
*
* @param deviceType the device type
* @param path the derivation path
* @return the xpub at the given derivation path
* @throws DeviceException if an error occurs
*/
public ExtendedKey getPubKeyAtPath(String deviceType, String path) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType);
return hardwareClient.getPubKeyAtPath(path);
}
}
/**
* Retrieves the xpub at the given path.
*
* @param deviceType the device type
* @param devicePath this device path
* @param path the derivation path
* @return the xpub at the given derivation path
* @throws DeviceException if an error occurs
*/
public ExtendedKey getPubKeyAtPath(String deviceType, String devicePath, String path) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType, devicePath);
return hardwareClient.getPubKeyAtPath(path);
}
}
/**
* Retrieves the xpub at the given path.
*
* @param fingerprint the device master fingerprint
* @param path the derivation path
* @return the xpub at the given derivation path
* @throws DeviceException if an error occurs
*/
public ExtendedKey getPubKeyAtPath(byte[] fingerprint, String path) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(fingerprint);
return hardwareClient.getPubKeyAtPath(path);
}
}
/**
* Signs the provided PSBT.
*
* @param deviceType the device type
* @param psbt the PSBT to be signed
* @return the signed PSBT
* @throws DeviceException if an error occurs
*/
public PSBT signTransaction(String deviceType, PSBT psbt) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType);
return hardwareClient.signTransaction(psbt);
}
}
/**
* Signs the provided PSBT.
*
* @param deviceType the device type
* @param devicePath this device path
* @param psbt the PSBT to be signed
* @return the signed PSBT
* @throws DeviceException if an error occurs
*/
public PSBT signTransaction(String deviceType, String devicePath, PSBT psbt) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType, devicePath);
return hardwareClient.signTransaction(psbt);
}
}
/**
* Signs the provided PSBT.
*
* @param fingerprint the device master fingerprint
* @param psbt the PSBT to be signed
* @return the signed PSBT
* @throws DeviceException if an error occurs
*/
public PSBT signTransaction(byte[] fingerprint, PSBT psbt) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(fingerprint);
return hardwareClient.signTransaction(psbt);
}
}
/**
* Requests the device to sign the provided message using the address at the given path.
* Note that only legacy signing is supported.
*
* @param deviceType the device type
* @param message the message to be signed
* @param path the path to the address signing the message
* @return the signature
* @throws DeviceException if an error occurs
*/
public String signMessage(String deviceType, String message, String path) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType);
return hardwareClient.signMessage(message, path);
}
}
/**
* Requests the device to sign the provided message using the address at the given path.
* Note that only legacy signing is supported.
*
* @param deviceType the device type
* @param devicePath this device path
* @param message the message to be signed
* @param path the path to the address signing the message
* @return the signature
* @throws DeviceException if an error occurs
*/
public String signMessage(String deviceType, String devicePath, String message, String path) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType, devicePath);
return hardwareClient.signMessage(message, path);
}
}
/**
* Requests the device to sign the provided message using the address at the given path.
* Note that only legacy signing is supported.
*
* @param fingerprint the device master fingerprint
* @param message the message to be signed
* @param path the path to the address signing the message
* @return the signature
* @throws DeviceException if an error occurs
*/
public String signMessage(byte[] fingerprint, String message, String path) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(fingerprint);
return hardwareClient.signMessage(message, path);
}
}
/**
* Requests the device to display the address for the provided output descriptor.
*
* @param deviceType the device type
* @param outputDescriptor the output descriptor providing the full path to the address
* @return the address
* @throws DeviceException if an error occurs
*/
public String displayAddress(String deviceType, OutputDescriptor outputDescriptor) throws DeviceException {
if(outputDescriptor.isMultisig()) {
return displayMultisigAddress(deviceType, outputDescriptor);
} else {
ExtendedKey xpub = outputDescriptor.getSingletonExtendedPublicKey();
KeyDerivation addressDerivation = outputDescriptor.getKeyDerivation(xpub);
List<ChildNumber> childDerivation = outputDescriptor.getChildDerivation(xpub);
if(childDerivation != null) {
addressDerivation = addressDerivation.extend(childDerivation.subList(1, childDerivation.size()));
}
String path = addressDerivation.getDerivationPath();
ScriptType scriptType = outputDescriptor.getScriptType();
return displaySinglesigAddress(deviceType, path, scriptType);
}
}
/**
* Requests the device to display the address for the provided output descriptor.
*
* @param deviceType the device type
* @param devicePath this device path
* @param outputDescriptor the output descriptor providing the full path to the address
* @return the address
* @throws DeviceException if an error occurs
*/
public String displayAddress(String deviceType, String devicePath, OutputDescriptor outputDescriptor) throws DeviceException {
if(outputDescriptor.isMultisig()) {
return displayMultisigAddress(deviceType, devicePath, outputDescriptor);
} else {
ExtendedKey xpub = outputDescriptor.getSingletonExtendedPublicKey();
KeyDerivation addressDerivation = outputDescriptor.getKeyDerivation(xpub);
List<ChildNumber> childDerivation = outputDescriptor.getChildDerivation(xpub);
if(childDerivation != null) {
addressDerivation = addressDerivation.extend(childDerivation.subList(1, childDerivation.size()));
}
String path = addressDerivation.getDerivationPath();
ScriptType scriptType = outputDescriptor.getScriptType();
return displaySinglesigAddress(deviceType, devicePath, path, scriptType);
}
}
/**
* Requests the device to display the address for the provided output descriptor.
*
* @param fingerprint the device master fingerprint
* @param outputDescriptor the output descriptor providing the full path to the address
* @return the address
* @throws DeviceException if an error occurs
*/
public String displayAddress(byte[] fingerprint, OutputDescriptor outputDescriptor) throws DeviceException {
if(outputDescriptor.isMultisig()) {
return displayMultisigAddress(fingerprint, outputDescriptor);
} else {
ExtendedKey xpub = outputDescriptor.getSingletonExtendedPublicKey();
KeyDerivation addressDerivation = outputDescriptor.getKeyDerivation(xpub);
List<ChildNumber> childDerivation = outputDescriptor.getChildDerivation(xpub);
if(childDerivation != null) {
addressDerivation = addressDerivation.extend(childDerivation.subList(1, childDerivation.size()));
}
String path = addressDerivation.getDerivationPath();
ScriptType scriptType = outputDescriptor.getScriptType();
return displaySinglesigAddress(fingerprint, path, scriptType);
}
}
/**
* Requests the device to display the address for the provided path and script type.
*
* @param deviceType the device type
* @param path the full derivation path to the address
* @param scriptType the script type of the address
* @return the address
* @throws DeviceException if an error occurs
*/
public String displaySinglesigAddress(String deviceType, String path, ScriptType scriptType) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType);
return hardwareClient.displaySinglesigAddress(path, scriptType);
}
}
/**
* Requests the device to display the address for the provided path and script type.
*
* @param deviceType the device type
* @param devicePath the device path
* @param path the full derivation path to the address
* @param scriptType the script type of the address
* @return the address
* @throws DeviceException if an error occurs
*/
public String displaySinglesigAddress(String deviceType, String devicePath, String path, ScriptType scriptType) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType, devicePath);
return hardwareClient.displaySinglesigAddress(path, scriptType);
}
}
/**
* Requests the device to display the address for the provided path and script type.
*
* @param fingerprint the device master fingerprint
* @param path the full derivation path to the address
* @param scriptType the script type of the address
* @return the address
* @throws DeviceException if an error occurs
*/
public String displaySinglesigAddress(byte[] fingerprint, String path, ScriptType scriptType) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(fingerprint);
return hardwareClient.displaySinglesigAddress(path, scriptType);
}
}
private String displayMultisigAddress(String deviceType, OutputDescriptor outputDescriptor) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType);
return hardwareClient.displayMultisigAddress(outputDescriptor);
}
}
private String displayMultisigAddress(String deviceType, String devicePath, OutputDescriptor outputDescriptor) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType, devicePath);
return hardwareClient.displayMultisigAddress(outputDescriptor);
}
}
private String displayMultisigAddress(byte[] fingerprint, OutputDescriptor outputDescriptor) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(fingerprint);
return hardwareClient.displayMultisigAddress(outputDescriptor);
}
}
/**
* Asks the device to prompt for a PIN.
* Only applicable for Trezor One and KeepKey.
*
* @param deviceType the device type
* @return whether the operation was successful
* @throws DeviceException if an error occurs
*/
public synchronized boolean promptPin(String deviceType) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType);
return hardwareClient.promptPin();
}
}
/**
* Asks the device to prompt for a PIN.
* Only applicable for Trezor One and KeepKey.
*
* @param deviceType the device type
* @param devicePath this device path
* @return whether the operation was successful
* @throws DeviceException if an error occurs
*/
public synchronized boolean promptPin(String deviceType, String devicePath) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType, devicePath);
return hardwareClient.promptPin();
}
}
/**
* Asks the device to prompt for a PIN.
* Only applicable for Trezor One and KeepKey.
*
* @param fingerprint the device master fingerprint
* @return whether the operation was successful
* @throws DeviceException if an error occurs
*/
public synchronized boolean promptPin(byte[] fingerprint) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(fingerprint);
return hardwareClient.promptPin();
}
}
/**
* Sends a PIN to the device.
* Only applicable for Trezor One and KeepKey.
*
* @param deviceType the device type
* @param pin the device PIN
* @return whether the operation was successful
* @throws DeviceException if an error occurs
*/
public synchronized boolean sendPin(String deviceType, String pin) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType);
return hardwareClient.sendPin(pin);
}
}
/**
* Sends a PIN to the device.
* Only applicable for Trezor One and KeepKey.
*
* @param deviceType the device type
* @param devicePath this device path
* @param pin the device PIN
* @return whether the operation was successful
* @throws DeviceException if an error occurs
*/
public synchronized boolean sendPin(String deviceType, String devicePath, String pin) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType, devicePath);
return hardwareClient.sendPin(pin);
}
}
/**
* Sends a PIN to the device.
* Only applicable for Trezor One and KeepKey.
*
* @param fingerprint the device master fingerprint
* @param pin the device PIN
* @return whether the operation was successful
* @throws DeviceException if an error occurs
*/
public synchronized boolean sendPin(byte[] fingerprint, String pin) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(fingerprint);
return hardwareClient.sendPin(pin);
}
}
/**
* Toggles whether a BIP39 passphrase is requested by the device.
* Not applicable to all devices.
*
* @param deviceType the device type
* @return whether the operation was successful
* @throws DeviceException if an error occurs
*/
public synchronized boolean togglePassphrase(String deviceType) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType);
return hardwareClient.togglePassphrase();
}
}
/**
* Toggles whether a BIP39 passphrase is requested by the device.
* Not applicable to all devices.
*
* @param deviceType the device type
* @param devicePath this device path
* @return whether the operation was successful
* @throws DeviceException if an error occurs
*/
public synchronized boolean togglePassphrase(String deviceType, String devicePath) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(deviceType, devicePath);
return hardwareClient.togglePassphrase();
}
}
/**
* Toggles whether a BIP39 passphrase is requested by the device.
* Not applicable to all devices.
*
* @param fingerprint the device master fingerprint
* @return whether the operation was successful
* @throws DeviceException if an error occurs
*/
public synchronized boolean togglePassphrase(byte[] fingerprint) throws DeviceException {
synchronized(lock) {
HardwareClient hardwareClient = getHardwareClient(fingerprint);
return hardwareClient.togglePassphrase();
}
}
public static HttpClientService getHttpClientService() {
return httpClientService;
}
public static boolean isConsoleOutput() {
return consoleOutput;
}
/**
* Sets the noise configuration for the BitBox02 client.
* This includes methods to complete pairing a device to this client if necessary.
*
* @param bitBoxNoiseConfig the noise configuration
*/
public void setBitBoxNoiseConfig(BitBoxNoiseConfig bitBoxNoiseConfig) {
this.bitBoxNoiseConfig = bitBoxNoiseConfig;
}
/**
* Sets the passphrase to use with the device, if one can be set from this client.
*
* @param passphrase the BIP39 passphrase
*/
public void setPassphrase(String passphrase) {
this.passphrase = passphrase;
}
/**
* Gets any names that have been set.
*
* @return a map of output descriptors to wallet names
*/
public Map<OutputDescriptor, String> getWalletNames() {
return Collections.unmodifiableMap(walletNames);
}
/**
* Provides a name for a wallet if one is registered on a device
*
* @param outputDescriptor output descriptor identifying the wallet
* @param name the wallet name
*/
public void addWalletName(OutputDescriptor outputDescriptor, String name) {
walletNames.put(outputDescriptor.copy(false), name);
}
/**
* Gets the wallet name for a given output descriptor, if set.
*
* @param outputDescriptor output descriptor identifying the wallet
* @return the wallet name
*/
public String getWalletName(OutputDescriptor outputDescriptor) {
return walletNames.get(outputDescriptor.copy(false));
}
/**
* Gets any registrations that have been set, either by this API or a device.
*
* @return a map of output descriptors to byte arrays representing an internal ids used by a device
*/
public Map<OutputDescriptor, byte[]> getWalletRegistrations() {
return Collections.unmodifiableMap(walletRegistrations);
}
/**
* Provides details of an existing wallet registered on a device.
* This is used for Ledger devices. Note that the name provided MUST be the same as that provided when the wallet was originally registered!
*
* @param outputDescriptor output descriptor identifying the wallet
* @param name the wallet name
* @param registration a byte array representing an internal id used by a device
*/
public void addWalletRegistration(OutputDescriptor outputDescriptor, String name, byte[] registration) {
addWalletName(outputDescriptor, name);
walletRegistrations.put(outputDescriptor.copy(false), registration);
}
/**
* Gets a wallet registration that has been set, either by this API or a device.
*
* @param outputDescriptor output descriptor identifying the wallet
* @return a byte array representing an internal id used by a device
*/
public byte[] getWalletRegistration(OutputDescriptor outputDescriptor) {
return walletRegistrations.get(outputDescriptor.copy(false));
}
public static void main(String[] argv) throws Exception {
consoleOutput = true;
List<Command> commands = List.of(
new EnumerateCommand(),
new PromptPinCommand(),
new SendPinCommand(),
new GetXpubCommand(),
new GetMasterXpubCommand(),
new SignTxCommand(),
new SignMessageCommand(),
new DisplayAddressCommand(),
new TogglePassphraseCommand());
Args args = new Args();
JCommander.Builder jCommanderBuilder = JCommander.newBuilder()
.addObject(args)
.programName(APP_NAME.toLowerCase(Locale.ROOT));
for(Command command : commands) {
jCommanderBuilder.addCommand(command.getName(), command);
}
JCommander jCommander = jCommanderBuilder.build();
try {
jCommander.parse(argv);
} catch(ParameterException e) {
showErrorAndExit(e.getMessage());
}
if(args.help) {
jCommander.usage();
System.exit(0);
}
if(args.version) {
System.out.println(APP_NAME + " " + APP_VERSION);
System.exit(0);
}
if(args.level != null) {
Drongo.setRootLogLevel(args.level);
} else if(args.debug) {
Drongo.setRootLogLevel(Level.DEBUG);
}
if(args.network != null) {
Network.set(args.network);
} else if(args.chain != null) {
Network.set(args.chain.getNetwork());
}
Lark lark = new Lark();
if(args.passphrase != null) {
lark.setPassphrase(args.passphrase);
}
if(args.walletRegistration != null) {
if(args.walletDescriptor == null || args.walletName == null) {
System.err.println("If `--wallet-registration` is provided, `--wallet-descriptor` and `--wallet-name` must also be provided");
System.exit(1);
}
OutputDescriptor walletDescriptor = getWalletDescriptor(args);
byte[] walletRegistration = getWalletRegistration(args);
lark.addWalletRegistration(walletDescriptor, args.walletName, walletRegistration);
} else if(args.walletName != null) {
if(args.walletDescriptor == null) {
System.err.println("If `--wallet-name` is provided, `--wallet-descriptor` must also be provided");
System.exit(1);
}
OutputDescriptor walletDescriptor = getWalletDescriptor(args);
lark.addWalletName(walletDescriptor, args.walletName);
}
try {
for(Command command : commands) {
if(command.getName().equals(jCommander.getParsedCommand())) {
command.run(jCommander, lark, args);
}
}
} catch(DeviceException e) {
showErrorAndExit(e.getMessage());
}
}
private static OutputDescriptor getWalletDescriptor(Args args) {
try {
return OutputDescriptor.getOutputDescriptor(args.walletDescriptor);
} catch(Exception e) {
System.err.println("Invalid wallet descriptor: " + e.getMessage());
System.exit(1);
return null;
}
}
private static byte[] getWalletRegistration(Args args) {
try {
return Utils.hexToBytes(args.walletRegistration);
} catch(Exception e) {
System.err.println("Invalid wallet registration: " + e.getMessage());
System.exit(1);
return null;
}
}
public static void showSuccess(boolean success) {
try {
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(new Lark.Success(success)));
} catch(JsonProcessingException e) {
log.error("Failed to serialize error", e);
}
}
public static void showValue(Object value) {
try {
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(value));
} catch(JsonProcessingException e) {
log.error("Failed to serialize error", e);
}
}
public static void showErrorAndExit(String errorMessage) {
try {
ObjectMapper objectMapper = new ObjectMapper();
System.err.println(objectMapper.writeValueAsString(new Error(errorMessage)));
System.exit(1);
} catch(JsonProcessingException e) {
log.error("Failed to serialize error", e);
}
}
private record Success(boolean success) {
}
private record Error(String error) {
}
}

View File

@ -0,0 +1,457 @@
package com.sparrowwallet.lark;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.ledger.*;
import com.sparrowwallet.lark.ledger.wallet.MultisigWalletPolicy;
import com.sparrowwallet.lark.ledger.wallet.WalletPolicy;
import com.sparrowwallet.lark.ledger.wallet.WalletType;
import org.hid4java.HidDevice;
import java.util.*;
public class LedgerClient extends HardwareClient {
private final HidDevice hidDevice;
private final LedgerModel ledgerModel;
private String masterFingerprint;
private Map<OutputDescriptor, byte[]> walletRegistrations = new HashMap<>();
public LedgerClient(HidDevice hidDevice) throws DeviceException {
if(LedgerModel.getDeviceIds().stream().anyMatch(deviceId -> deviceId.getVendorId() == hidDevice.getVendorId() && deviceId.getProductId() == hidDevice.getProductId() >> 8) &&
(hidDevice.getUsagePage() == 0xFFA0 || hidDevice.getInterfaceNumber() == 0)) {
this.hidDevice = hidDevice;
this.ledgerModel = LedgerModel.getLedgerModel(hidDevice.getProductId() >> 8);
} else {
throw new DeviceException("Not a Ledger");
}
}
private LedgerDevice getLedgerDevice(HidDevice hidDevice) throws DeviceException {
NewLedgerDevice newLedgerDevice = new NewLedgerDevice(new HIDTransport(hidDevice));
LedgerDevice.LedgerVersion ledgerVersion = newLedgerDevice.getVersion();
return ledgerVersion.isLegacy() ? new LegacyLedgerDevice(new HIDTransport(hidDevice), ledgerVersion) : newLedgerDevice;
}
@Override
void initializeMasterFingerprint() throws DeviceException {
try(LedgerDevice ledgerDevice = getLedgerDevice(hidDevice)) {
getMasterFingerprint(ledgerDevice);
}
}
private void getMasterFingerprint(LedgerDevice ledgerDevice) throws DeviceException {
this.masterFingerprint = ledgerDevice.getMasterFingerprint();
}
@Override
ExtendedKey getPubKeyAtPath(String path) throws DeviceException {
try(LedgerDevice ledgerDevice = getLedgerDevice(hidDevice)) {
return ledgerDevice.getExtendedPubkey(path, false);
}
}
@Override
PSBT signTransaction(PSBT psbt) throws DeviceException {
try(LedgerDevice ledgerDevice = getLedgerDevice(hidDevice)) {
getMasterFingerprint(ledgerDevice);
if(ledgerDevice instanceof LegacyLedgerDevice) {
WalletPolicy walletPolicy = new WalletPolicy("", "wpkh(@0/**)", List.of(""));
List<LedgerDevice.Signature> signatures = ledgerDevice.signPsbt(psbt, walletPolicy, null);
for(LedgerDevice.Signature signature : signatures) {
PSBTInput psbtInput = psbt.getPsbtInputs().get(signature.inputIndex());
psbtInput.getPartialSignatures().put(signature.ecKey(), signature.transactionSignature());
}
return psbt;
} else {
PSBT psbt2 = new PSBT(psbt.serialize());
if(psbt2.getVersion() == null || psbt2.getVersion() == 0) {
psbt2.convertVersion(2);
}
Map<Sha256Hash, Wallet> wallets = new HashMap<>();
Map<Integer, ECKey> pubkeys = new HashMap<>();
for(int inputIndex = 0; inputIndex < psbt2.getPsbtInputs().size(); inputIndex++) {
PSBTInput psbtInput = psbt2.getPsbtInputs().get(inputIndex);
TransactionOutput utxo = null;
Script script;
if(psbtInput.getWitnessUtxo() != null) {
utxo = psbtInput.getWitnessUtxo();
}
if(psbtInput.getNonWitnessUtxo() != null) {
utxo = psbtInput.getNonWitnessUtxo().getOutputs().get(psbtInput.getPrevIndex().intValue());
psbtInput.setWitnessUtxo(utxo);
}
if(utxo == null) {
continue;
}
script = utxo.getScript();
boolean p2sh = false;
if(ScriptType.P2SH.isScriptType(script)) {
if(psbtInput.getRedeemScript() == null) {
continue;
}
script = psbtInput.getRedeemScript();
p2sh = true;
}
ScriptType scriptType = ScriptType.P2PKH;
Optional<ScriptType> optWitnessType = isWitness(script);
if(optWitnessType.isPresent()) {
ScriptType witnessType = optWitnessType.get();
if(p2sh) {
if(witnessType.equals(ScriptType.P2WPKH)) {
scriptType = ScriptType.P2SH_P2WPKH;
} else if(witnessType.equals(ScriptType.P2WSH)) {
scriptType = ScriptType.P2SH_P2WSH;
} else {
throw new IllegalArgumentException("Cannot have witness v1+ in " + ScriptType.P2SH);
}
} else {
if(witnessType.equals(ScriptType.P2WPKH)) {
scriptType = ScriptType.P2WPKH;
} else if(witnessType.equals(ScriptType.P2WSH)) {
scriptType = ScriptType.P2WSH;
} else if(witnessType.equals(ScriptType.P2TR)) {
scriptType = ScriptType.P2TR;
} else {
continue;
}
}
}
if(ScriptType.P2WSH.isScriptType(script)) {
if(psbtInput.getWitnessScript() == null) {
continue;
}
script = psbtInput.getWitnessScript();
}
if(ScriptType.MULTISIG.isScriptType(script)) {
int m = ScriptType.MULTISIG.getThreshold(script);
ECKey[] keys = ScriptType.MULTISIG.getPublicKeysFromScript(script);
List<String> keyExprs = new ArrayList<>();
Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new HashMap<>();
boolean ok = true;
for(ECKey key : keys) {
if(psbtInput.getDerivedPublicKeys().containsKey(key)) {
KeyDerivation origin = psbtInput.getDerivedPublicKeys().get(key);
if(masterFingerprint.equals(origin.getMasterFingerprint())) {
pubkeys.put(inputIndex, key);
}
for(ExtendedKey xpub : psbt2.getExtendedPublicKeys().keySet()) {
KeyDerivation xpubOrigin = psbt2.getExtendedPublicKeys().get(xpub);
if(xpubOrigin.getMasterFingerprint().equals(origin.getMasterFingerprint()) &&
xpubOrigin.getDerivation().equals(origin.getDerivation().subList(0, xpubOrigin.getDerivation().size()))) {
keyExprs.add(OutputDescriptor.writeKey(xpub, xpubOrigin, null, true, true, true));
extendedPublicKeys.put(xpub, xpubOrigin);
break;
}
}
} else {
ok = false;
}
}
if(!ok) {
continue;
}
if(scriptType == ScriptType.P2PKH) {
scriptType = ScriptType.P2SH;
}
OutputDescriptor walletDescriptor = new OutputDescriptor(scriptType, m, extendedPublicKeys);
MultisigWalletPolicy mswp = new MultisigWalletPolicy(getWalletNameOrDefault(walletDescriptor, psbt), scriptType, m, keyExprs);
Sha256Hash mswpId = mswp.id();
if(!wallets.containsKey(mswpId)) {
Sha256Hash registeredWalletId = getWalletRegistration(ledgerDevice, walletDescriptor, mswp);
wallets.put(mswpId, new Wallet(SigningPriority.fromScriptType(scriptType), scriptType, mswp, registeredWalletId));
}
} else {
for(ECKey key : psbtInput.getDerivedPublicKeys().keySet()) {
KeyDerivation origin = psbtInput.getDerivedPublicKeys().get(key);
if(masterFingerprint.equals(origin.getMasterFingerprint())) {
if(!ScriptType.MULTISIG.isScriptType(script)) {
processOrigin(ledgerDevice, wallets, scriptType, origin);
}
pubkeys.put(inputIndex, key);
}
}
for(ECKey key : psbtInput.getTapDerivedPublicKeys().keySet()) {
Map<KeyDerivation, List<Sha256Hash>> keypath = psbtInput.getTapDerivedPublicKeys().get(key);
for(KeyDerivation origin : keypath.keySet()) {
//Note script path signing is not currently supported
if(key.equals(psbtInput.getTapInternalKey()) && origin.getMasterFingerprint().equals(masterFingerprint)) {
processOrigin(ledgerDevice, wallets, scriptType, origin);
pubkeys.put(inputIndex, key);
}
}
}
}
}
// For each wallet, sign
List<Wallet> sortedWallets = new ArrayList<>(wallets.values());
sortedWallets.sort(Comparator.comparing(o -> o.signingPriority));
for(Wallet wallet : sortedWallets) {
if(wallet.scriptType() == ScriptType.P2PKH || wallet.scriptType() == ScriptType.P2SH) {
for(PSBTInput psbtInput : psbt2.getPsbtInputs()) {
TransactionOutput utxo = null;
if(psbtInput.getWitnessUtxo() != null) {
utxo = psbtInput.getWitnessUtxo();
}
if(utxo == null) {
continue;
}
Optional<ScriptType> optWitness = isWitness(utxo.getScript());
if(optWitness.isEmpty()) {
psbtInput.setWitnessUtxo(null);
}
}
}
List<LedgerDevice.Signature> signatures = ledgerDevice.signPsbt(psbt2, wallet.walletPolicy(), wallet.registeredHmac());
for(LedgerDevice.Signature signature : signatures) {
PSBTInput psbtInput = psbt2.getPsbtInputs().get(signature.inputIndex());
TransactionOutput utxo = null;
if(psbtInput.getWitnessUtxo() != null) {
utxo = psbtInput.getWitnessUtxo();
}
if(psbtInput.getNonWitnessUtxo() != null) {
utxo = psbtInput.getNonWitnessUtxo().getOutputs().get(psbtInput.getPrevIndex().intValue());
}
if(utxo == null) {
throw new IllegalArgumentException("The previous transaction output must be provided");
}
Optional<ScriptType> optWitness = isWitness(utxo.getScript());
if(optWitness.isPresent() && optWitness.get() == ScriptType.P2TR) {
//Keypath signature is assumed
psbtInput.setTapKeyPathSignature(signature.transactionSignature());
} else {
psbtInput.getPartialSignatures().put(signature.ecKey(), signature.transactionSignature());
}
}
}
for(int inputIndex = 0; inputIndex < psbt2.getPsbtInputs().size(); inputIndex++) {
PSBTInput psbtInput = psbt2.getPsbtInputs().get(inputIndex);
PSBTInput origInput = psbt.getPsbtInputs().get(inputIndex);
origInput.getPartialSignatures().putAll(psbtInput.getPartialSignatures());
if(psbtInput.getTapKeyPathSignature() != null && origInput.getTapKeyPathSignature() == null) {
origInput.setTapKeyPathSignature(psbtInput.getTapKeyPathSignature());
}
}
return psbt;
}
} catch(PSBTParseException e) {
throw new IllegalArgumentException("Could not reparse PSBT", e);
}
}
private Sha256Hash getWalletRegistration(LedgerDevice ledgerDevice, OutputDescriptor outputDescriptor, MultisigWalletPolicy mswp) throws DeviceException {
OutputDescriptor walletDescriptor = outputDescriptor.copy(false);
if(walletRegistrations.containsKey(walletDescriptor)) {
if(walletRegistrations.get(walletDescriptor).length != 32) {
throw new IllegalStateException("Wallet registration was not 32 bytes");
}
return Sha256Hash.wrap(walletRegistrations.get(walletDescriptor));
}
LedgerDevice.WalletRegistration registration = ledgerDevice.registerWallet(mswp);
Sha256Hash registeredWalletId = registration.hmac();
walletRegistrations.put(walletDescriptor, registeredWalletId.getBytes());
return registeredWalletId;
}
private void processOrigin(LedgerDevice ledgerDevice, Map<Sha256Hash, Wallet> wallets, ScriptType scriptType, KeyDerivation origin) throws DeviceException {
if(!isStandardPath(origin.getDerivation(), scriptType)) {
//Non default wallets are not currently supported
return;
}
WalletPolicy walletPolicy = getSingleSigWalletPolicy(ledgerDevice, scriptType, origin.getDerivation().get(2).num());
wallets.put(walletPolicy.id(), new Wallet(SigningPriority.fromScriptType(scriptType), scriptType, walletPolicy, null));
}
private boolean isStandardPath(List<ChildNumber> path, ScriptType scriptType) {
List<ChildNumber> standard = scriptType.getDefaultDerivation();
if(path.size() != 5) {
return false;
}
if(!path.stream().limit(3).allMatch(ChildNumber::isHardened)) {
return false;
}
if(path.get(3).isHardened() || path.get(4).isHardened()) {
return false;
}
if(!path.get(0).equals(standard.get(0))) {
return false;
}
if(Network.get() == Network.MAINNET && !path.get(1).equals(ChildNumber.ZERO_HARDENED)) {
return false;
}
if(Network.get() != Network.MAINNET && !path.get(1).equals(ChildNumber.ONE_HARDENED)) {
return false;
}
if(!path.get(3).equals(ChildNumber.ZERO) && !path.get(3).equals(ChildNumber.ONE)) {
return false;
}
return true;
}
private WalletPolicy getSingleSigWalletPolicy(LedgerDevice ledgerDevice, ScriptType scriptType, int account) throws DeviceException {
String template = switch(scriptType) {
case P2PKH -> "pkh(@0/**)";
case P2WPKH -> "wpkh(@0/**)";
case P2SH_P2WPKH -> "sh(wpkh(@0/**))";
case P2TR -> "tr(@0/**)";
default -> throw new IllegalStateException("Unexpected script type: " + scriptType);
};
List<ChildNumber> path = scriptType.getDefaultDerivation(account);
KeyDerivation origin = new KeyDerivation(this.masterFingerprint, path);
ExtendedKey xpub = ledgerDevice.getExtendedPubkey(origin.getDerivationPath(), false);
String keyExpr = OutputDescriptor.writeKey(xpub, origin, null, true, true, true);
return new WalletPolicy("", template, List.of(keyExpr));
}
@Override
String signMessage(String message, String path) throws DeviceException {
List<ChildNumber> keyPath = KeyDerivation.parsePath(path);
String rewrittenPath = KeyDerivation.writePath(keyPath, true);
try(LedgerDevice ledgerDevice = getLedgerDevice(hidDevice)) {
return ledgerDevice.signMessage(message, rewrittenPath);
}
}
@Override
String displaySinglesigAddress(String path, ScriptType scriptType) throws DeviceException {
try(LedgerDevice ledgerDevice = getLedgerDevice(hidDevice)) {
List<ChildNumber> keyPath = KeyDerivation.parsePath(path);
getMasterFingerprint(ledgerDevice);
WalletPolicy walletPolicy;
if(ledgerDevice instanceof LegacyLedgerDevice) {
String template = switch(scriptType) {
case P2PKH -> "pkh(@0/**)";
case P2WPKH -> "wpkh(@0/**)";
case P2SH_P2WPKH -> "sh(wpkh(@0/**))";
case P2TR -> throw new DeviceException("Taproot is not supported by this version of the Bitcoin App");
default -> throw new IllegalArgumentException("Unexpected script type: " + scriptType);
};
String keysInfo = OutputDescriptor.writeKey(null, new KeyDerivation(masterFingerprint, path), null, true, false, true);
walletPolicy = new WalletPolicy("", template, List.of(keysInfo));
} else {
if(!isStandardPath(keyPath, scriptType)) {
throw new DeviceException("Ledger requires BIP 44 standard paths");
}
walletPolicy = getSingleSigWalletPolicy(ledgerDevice, scriptType, keyPath.get(2).num());
}
return ledgerDevice.getWalletAddress(walletPolicy, null, keyPath.get(keyPath.size() - 2).num(), keyPath.get(keyPath.size() - 1).num(), true);
}
}
@Override
String displayMultisigAddress(OutputDescriptor outputDescriptor) throws DeviceException {
try(LedgerDevice ledgerDevice = getLedgerDevice(hidDevice)) {
if(ledgerDevice instanceof LegacyLedgerDevice) {
throw new DeviceException("Displaying multisignature addresses is not supported by this version of the Bitcoin App");
}
if(!outputDescriptor.getExtendedPublicKeys().stream().map(outputDescriptor::getChildDerivation).allMatch(this::isValidDerivationPath)) {
throw new DeviceException("Ledger Bitcoin app requires all derivation paths to end with /0/*, or all with /1/* for multisig");
}
if(outputDescriptor.getExtendedPublicKeys().stream().map(outputDescriptor::getKeyDerivation).anyMatch(kd -> kd.getDerivation().size() > 4)) {
throw new DeviceException("Ledger Bitcoin app requires extended keys with derivation length at most 4");
}
List<String> keysInfo = outputDescriptor.sortExtendedPubKeys(outputDescriptor.getExtendedPublicKeys())
.stream().map(xpub -> OutputDescriptor.writeKey(xpub, outputDescriptor.getKeyDerivation(xpub), null, true, true, true)).toList();
MultisigWalletPolicy mswp = new MultisigWalletPolicy(getWalletNameOrDefault(outputDescriptor), outputDescriptor.getScriptType(),
outputDescriptor.getMultisigThreshold(), keysInfo, true, WalletType.WALLET_POLICY_V2);
Sha256Hash registeredWalletId = getWalletRegistration(ledgerDevice, outputDescriptor, mswp);
List<ChildNumber> childPath = outputDescriptor.getChildDerivation(outputDescriptor.getExtendedPublicKeys().iterator().next());
return ledgerDevice.getWalletAddress(mswp, registeredWalletId, childPath.get(1).num(), childPath.get(2).num(), true);
}
}
private boolean isValidDerivationPath(List<ChildNumber> derivationPath) {
return derivationPath.size() == 3 && (derivationPath.get(1).equals(ChildNumber.ZERO) || derivationPath.get(1).equals(ChildNumber.ONE)) && !derivationPath.get(2).isHardened();
}
@Override
public String getPath() {
return hidDevice.getPath();
}
@Override
public HardwareType getHardwareType() {
return HardwareType.LEDGER;
}
@Override
public String getProductModel() {
return ledgerModel.getName();
}
@Override
public WalletModel getModel() {
return ledgerModel.getWalletModel();
}
@Override
public Boolean needsPinSent() {
return masterFingerprint == null ? null : false;
}
@Override
public Boolean needsPassphraseSent() {
return masterFingerprint == null ? null : false;
}
@Override
public String fingerprint() {
return masterFingerprint;
}
@Override
public boolean card() {
return false;
}
@Override
public String[][] warnings() {
return new String[0][];
}
public void setWalletRegistrations(Map<OutputDescriptor, byte[]> walletRegistrations) {
this.walletRegistrations = walletRegistrations;
}
private record Wallet(SigningPriority signingPriority, ScriptType scriptType, WalletPolicy walletPolicy, Sha256Hash registeredHmac) {}
}

View File

@ -0,0 +1,48 @@
package com.sparrowwallet.lark;
public enum Platform {
WINDOWS("Windows"),
MACOS("macOS"),
UNIX("Unix"),
UNKNOWN("");
private static final Platform current = getCurrentPlatform();
private final String platformId;
Platform(String platformId) {
this.platformId = platformId;
}
/**
* Returns platform id. Usually used to specify platform dependent styles
*
* @return platform id
*/
public String getPlatformId() {
return platformId;
}
/**
* @return the current OS.
*/
public static Platform getCurrent() {
return current;
}
private static Platform getCurrentPlatform() {
String osName = System.getProperty("os.name");
if(osName.startsWith("Windows")) {
return WINDOWS;
}
if(osName.startsWith("Mac")) {
return MACOS;
}
if(osName.startsWith("SunOS")) {
return UNIX;
}
if(osName.startsWith("Linux")) {
return UNIX;
}
return UNKNOWN;
}
}

View File

@ -0,0 +1,754 @@
package com.sparrowwallet.lark;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.trezor.PassphraseUI;
import com.sparrowwallet.lark.trezor.TrezorDevice;
import com.sparrowwallet.lark.trezor.TrezorModel;
import com.sparrowwallet.lark.trezor.generated.TrezorMessageBitcoin;
import com.sparrowwallet.lark.trezor.generated.TrezorMessageCommon;
import com.sparrowwallet.lark.trezor.generated.TrezorMessageManagement;
import org.usb4java.Device;
import org.usb4java.DeviceDescriptor;
import org.usb4java.LibUsb;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.stream.IntStream;
public class TrezorClient extends HardwareClient {
public static final List<DeviceId> TREZOR_DEVICE_IDS = List.of(new DeviceId(0x534C, 0x0001),
new DeviceId(0x1209, 0x53C1), new DeviceId(0x1209, 0x53C0));
private static final String PATH_PREFIX = "webusb";
public static final String PIN_MATRIX_DESCRIPTION = """
Use the numeric keypad to describe number positions. The layout is:
7 8 9
4 5 6
1 2 3
""".strip();
private final List<TrezorMessageBitcoin.InputScriptType> ECDSA_SCRIPT_TYPES = List.of(
TrezorMessageBitcoin.InputScriptType.SPENDADDRESS,
TrezorMessageBitcoin.InputScriptType.SPENDMULTISIG,
TrezorMessageBitcoin.InputScriptType.SPENDWITNESS,
TrezorMessageBitcoin.InputScriptType.SPENDP2SHWITNESS
);
private final List<TrezorMessageBitcoin.InputScriptType> SCHNORR_SCRIPT_TYPES = List.of(
TrezorMessageBitcoin.InputScriptType.SPENDTAPROOT
);
private final Device device;
private final int busNumber;
private final ByteBuffer portNumbers = ByteBuffer.allocateDirect(7);
private String passphrase = "";
private WalletModel model;
private TrezorModel trezorModel;
private String label;
private Boolean needsPinSent;
private Boolean needsPassphraseSent;
private String masterFingerprint;
private final List<String> warnings = new ArrayList<>();
public TrezorClient(Device device, DeviceDescriptor deviceDescriptor) throws DeviceException {
this(TREZOR_DEVICE_IDS, device, deviceDescriptor);
}
protected TrezorClient(List<DeviceId> deviceIds, Device device, DeviceDescriptor deviceDescriptor) throws DeviceException {
if(deviceIds.stream().anyMatch(deviceId -> deviceId.matches(deviceDescriptor))) {
this.device = device;
this.busNumber = LibUsb.getBusNumber(device);
LibUsb.getPortNumbers(device, portNumbers);
} else {
throw new DeviceException("Not a " + getHardwareType().getDisplayName());
}
}
private void prepareDevice(TrezorDevice trezorDevice) throws DeviceException {
trezorDevice.refreshFeatures();
if(trezorDevice.getModel() == TrezorModel.T1B1 || trezorDevice.getModel() == TrezorModel.KEEPKEY) {
trezorDevice.initDevice();
} else {
try {
trezorDevice.ensureUnlocked();
} catch(DeviceException e) {
trezorDevice.initDevice();
}
}
this.trezorModel = trezorDevice.getModel();
this.model = trezorDevice.getModel().getWalletModel();
this.label = trezorDevice.getFeatures().getLabel();
this.needsPinSent = trezorDevice.getFeatures().getPinProtection() && !trezorDevice.getFeatures().getUnlocked();
if(trezorDevice.getModel().equals(TrezorModel.T1B1)) {
this.needsPassphraseSent = trezorDevice.getFeatures().getPassphraseProtection();
} else {
this.needsPassphraseSent = false;
}
if(needsPinSent) {
throw new DeviceNotReadyException(getHardwareType().getDisplayName() + " is locked. Unlock by using 'promptpin' and then 'sendpin'.");
}
if(needsPassphraseSent && passphrase == null) {
this.warnings.add("Passphrase protection enabled but passphrase was not provided. Using default passphrase of the empty string (\"\")");
}
if(trezorDevice.getFeatures().getInitialized()) {
initializeMasterFingerprint(trezorDevice);
this.needsPassphraseSent = false; //Passphrase is always needed for the above to have worked, so it's already sent
} else {
throw new DeviceNotReadyException(getHardwareType().getDisplayName() + " is not initialized.");
}
}
private void checkUnlocked(TrezorDevice trezorDevice) throws DeviceException {
prepareDevice(trezorDevice);
if(trezorDevice.getFeatures().getCapabilitiesList().stream().anyMatch(TrezorMessageManagement.Features.Capability.Capability_PassphraseEntry::equals)
&& trezorDevice.getUI() instanceof PassphraseUI) {
trezorDevice.getUI().disallowPassphrase();
}
if(trezorDevice.getFeatures().getPinProtection() && !trezorDevice.getFeatures().getUnlocked()) {
throw new DeviceNotReadyException(getHardwareType().getDisplayName() + " is locked. Unlock by using 'promptpin' and then 'sendpin'.'");
}
if(trezorDevice.getFeatures().getPassphraseProtection() && passphrase == null) {
throw new DeviceException("Passphrase protection is enabled, passphrase must be provided");
}
}
private String getMasterFingerprint(TrezorDevice trezorDevice) throws DeviceException {
TrezorMessageBitcoin.PublicKey masterKey = trezorDevice.getPublicNode(Network.MAINNET, List.of(ChildNumber.ONE_HARDENED));
return Integer.toHexString(masterKey.getRootFingerprint());
}
@Override
void initializeMasterFingerprint() throws DeviceException {
try(TrezorDevice trezorDevice = new TrezorDevice(device, new PassphraseUI(passphrase))) {
prepareDevice(trezorDevice);
}
}
private void initializeMasterFingerprint(TrezorDevice trezorDevice) throws DeviceException {
this.masterFingerprint = getMasterFingerprint(trezorDevice);
}
@Override
ExtendedKey getPubKeyAtPath(String path) throws DeviceException {
try(TrezorDevice trezorDevice = new TrezorDevice(device, new PassphraseUI(passphrase))) {
checkUnlocked(trezorDevice);
TrezorMessageBitcoin.PublicKey publicKey = trezorDevice.getPublicNode(Network.get(), KeyDerivation.parsePath(path));
return ExtendedKey.fromDescriptor(publicKey.getXpub());
}
}
/**
* Sign a transaction with the Trezor. There are some limitations to what transactions can be signed.
* - Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation.
* - Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation.
* - Send-to-self transactions will result in no prompt for outputs as all outputs will be detected as change.
* - Transactions containing Taproot inputs cannot have external inputs.
*
* @param psbt the PSBT to be signed
* @return the signed PSBT
* @throws DeviceException on an error
*/
@Override
PSBT signTransaction(PSBT psbt) throws DeviceException {
try(TrezorDevice trezorDevice = new TrezorDevice(device, new PassphraseUI(passphrase))) {
checkUnlocked(trezorDevice);
int passes = 1;
int p = 0;
while(p < passes) {
List<TrezorMessageBitcoin.TxInput> inputs = new ArrayList<>();
List<Integer> toIgnore = new ArrayList<>();
for(int inputIndex = 0; inputIndex < psbt.getPsbtInputs().size(); inputIndex++) {
PSBTInput psbtInput = psbt.getPsbtInputs().get(inputIndex);
TrezorMessageBitcoin.TxInput.Builder txInput = TrezorMessageBitcoin.TxInput.newBuilder()
.setPrevHash(ByteString.copyFrom(Utils.reverseBytes(serUInt256(psbtInput.getInput().getOutpoint().getHash().toBigInteger()))))
.setPrevIndex((int)psbtInput.getInput().getOutpoint().getIndex())
.setSequence((int)psbtInput.getInput().getSequenceNumber());
//Determine spend type
TransactionOutput utxo = psbtInput.getUtxo();
if(utxo == null) {
continue;
}
Script script = utxo.getScript();
//Check if P2SH
boolean p2sh = false;
if(ScriptType.P2SH.isScriptType(script)) {
//Lookup redeem script
if(psbtInput.getRedeemScript() == null) {
continue;
}
script = psbtInput.getRedeemScript();
p2sh = true;
}
//Check if segwit
Script inputScript = script;
Optional<ScriptType> optWitnessType = isWitness(inputScript);
if(optWitnessType.isPresent()) {
ScriptType witnessType = optWitnessType.get();
if(witnessType.equals(ScriptType.P2WPKH) || witnessType.equals(ScriptType.P2WSH)) {
if(p2sh) {
txInput.setScriptType(TrezorMessageBitcoin.InputScriptType.SPENDP2SHWITNESS);
} else {
txInput.setScriptType(TrezorMessageBitcoin.InputScriptType.SPENDWITNESS);
}
} else if(witnessType.equals(ScriptType.P2TR)) {
txInput.setScriptType(TrezorMessageBitcoin.InputScriptType.SPENDTAPROOT);
}
} else {
txInput.setScriptType(TrezorMessageBitcoin.InputScriptType.SPENDADDRESS);
}
txInput.setAmount(utxo.getValue());
//Check if P2WSH
boolean p2wsh = false;
if(ScriptType.P2WSH.isScriptType(script)) {
//Look up witnessScript
if(psbtInput.getWitnessScript() == null) {
continue;
}
script = psbtInput.getWitnessScript();
p2wsh = true;
}
//Check for multisig
Optional<TrezorMessageBitcoin.MultisigRedeemScriptType> optMultisigRedeemScriptType = getMultisig(script, psbt.getExtendedPublicKeys(), psbtInput.getDerivedPublicKeys());
if(optMultisigRedeemScriptType.isPresent()) {
txInput.setMultisig(optMultisigRedeemScriptType.get());
if(optWitnessType.isEmpty()) {
if(ScriptType.P2SH.isScriptType(utxo.getScript())) {
txInput.setScriptType(TrezorMessageBitcoin.InputScriptType.SPENDMULTISIG);
} else {
//Cannot sign bare multisig, ignore it
if(!trezorDevice.supportsExternal()) {
throw new DeviceException("Cannot sign bare multisig inputs");
}
ignoreInput(inputs, toIgnore, inputIndex, txInput);
continue;
}
}
} else if(optWitnessType.isEmpty() && !ScriptType.P2PKH.isScriptType(script)) {
//Cannot sign unknown spk, ignore it
if(!trezorDevice.supportsExternal()) {
throw new DeviceException("Cannot sign unknown scripts");
}
ignoreInput(inputs, toIgnore, inputIndex, txInput);
continue;
} else if(optWitnessType.isPresent() && p2wsh) {
//Cannot sign unknown witness script, ignore it
if(!trezorDevice.supportsExternal()) {
throw new DeviceException("Cannot sign unknown witness versionss");
}
ignoreInput(inputs, toIgnore, inputIndex, txInput);
continue;
}
//Find key to sign with
boolean found = false; //Whether we have found a key to sign with
boolean foundInSigs = false; //Whether we have found one of our keys in the signatures
int ourKeys = 0;
List<ChildNumber> pathLastOurs = null; //The path of the last key that is ours. We will use this if we need to ignore this input because it is already signed.
if(ECDSA_SCRIPT_TYPES.contains(txInput.getScriptType())) {
for(ECKey key : psbtInput.getDerivedPublicKeys().keySet()) {
KeyDerivation keypath = psbtInput.getDerivedPublicKeys().get(key);
if(keypath.getMasterFingerprint().equals(masterFingerprint)) {
pathLastOurs = keypath.getDerivation();
if(psbtInput.getPartialSignatures().containsKey(key)) { //This key already has a signature
foundInSigs = true;
continue;
}
if(!found) { //This key does not have a signature, and we don't have a key to sign with yet
txInput.addAllAddressN(keypath.getDerivation().stream().map(ChildNumber::i).toList());
found = true;
}
ourKeys++;
}
}
} else if(SCHNORR_SCRIPT_TYPES.contains(txInput.getScriptType())) {
foundInSigs = psbtInput.getTapKeyPathSignature() != null;
for(ECKey key : psbtInput.getTapDerivedPublicKeys().keySet()) {
Map<KeyDerivation, List<Sha256Hash>> keypath = psbtInput.getTapDerivedPublicKeys().get(key);
for(KeyDerivation keypathKey : keypath.keySet()) {
//Note script path signing is not currently supported
if(key.equals(psbtInput.getTapInternalKey()) && keypathKey.getMasterFingerprint().equals(masterFingerprint)) {
pathLastOurs = keypathKey.getDerivation();
txInput.addAllAddressN(keypathKey.getDerivation().stream().map(ChildNumber::i).toList());
found = true;
ourKeys++;
break;
}
}
}
}
//Determine if we need to do more passes to sign everything
if(ourKeys > passes) {
passes = ourKeys;
}
if(!found && !foundInSigs) {
//This input is not one of ours
if(!trezorDevice.supportsExternal()) {
throw new DeviceException("Cannot sign external inputs");
}
ignoreInput(inputs, toIgnore, inputIndex, txInput);
continue;
} else if(!found && foundInSigs) {
//All of our keys are in partial_sigs, pick the first key that is ours, sign with it,
//and ignore whatever signature is produced for this input
if(pathLastOurs == null) {
throw new IllegalStateException("Cannot determine path for input " + inputIndex);
}
txInput.addAllAddressN(pathLastOurs.stream().map(ChildNumber::i).toList());
toIgnore.add(inputIndex);
}
inputs.add(txInput.build());
}
//Prepare outputs
List<TrezorMessageBitcoin.TxOutput> outputs = new ArrayList<>();
for(int outputIndex = 0; outputIndex < psbt.getPsbtOutputs().size(); outputIndex++) {
PSBTOutput psbtOutput = psbt.getPsbtOutputs().get(outputIndex);
TransactionOutput out = psbt.getTransaction().getOutputs().get(outputIndex);
TrezorMessageBitcoin.TxOutput.Builder txOutput = TrezorMessageBitcoin.TxOutput.newBuilder()
.setAmount(out.getValue())
.setScriptType(TrezorMessageBitcoin.OutputScriptType.PAYTOADDRESS);
Address address = out.getScript().getToAddress();
if(address != null) {
txOutput.setAddress(address.toString());
} else if(out.getScript().getChunks().size() >= 2 && out.getScript().getChunks().get(0).getOpcode() == ScriptOpCodes.OP_RETURN) {
txOutput.setScriptType(TrezorMessageBitcoin.OutputScriptType.PAYTOOPRETURN);
txOutput.setOpReturnData(ByteString.copyFrom(Arrays.copyOfRange(out.getScriptBytes(), 2, out.getScriptBytes().length)));
} else {
throw new IllegalArgumentException("Output " + outputIndex + " is not an address");
}
//Add the derivation path for change
Optional<ScriptType> optWitnessType = isWitness(out.getScript());
if(optWitnessType.isEmpty() || (optWitnessType.get().equals(ScriptType.P2WPKH) || optWitnessType.get().equals(ScriptType.P2WSH))) {
for(KeyDerivation keypath : psbtOutput.getDerivedPublicKeys().values()) {
if(!keypath.getMasterFingerprint().equals(masterFingerprint)) {
continue;
}
if(ScriptType.P2PKH.isScriptType(out.getScript())) {
txOutput.addAllAddressN(keypath.getDerivation().stream().map(ChildNumber::i).toList());
txOutput.clearAddress();
outputs.add(txOutput.build());
break;
} else if(optWitnessType.isPresent()) {
txOutput.setScriptType(TrezorMessageBitcoin.OutputScriptType.PAYTOWITNESS);
txOutput.addAllAddressN(keypath.getDerivation().stream().map(ChildNumber::i).toList());
txOutput.clearAddress();
} else if(ScriptType.P2SH.isScriptType(out.getScript()) && psbtOutput.getRedeemScript() != null) {
if(ScriptType.P2SH_P2WPKH.isScriptType(psbtOutput.getRedeemScript()) || ScriptType.P2SH_P2WSH.isScriptType(psbtOutput.getRedeemScript())) {
txOutput.setScriptType(TrezorMessageBitcoin.OutputScriptType.PAYTOP2SHWITNESS);
txOutput.addAllAddressN(keypath.getDerivation().stream().map(ChildNumber::i).toList());
txOutput.clearAddress();
}
}
}
} else if(optWitnessType.get().equals(ScriptType.P2TR)) {
for(ECKey key : psbtOutput.getTapDerivedPublicKeys().keySet()) {
Map<KeyDerivation, List<Sha256Hash>> keypath = psbtOutput.getTapDerivedPublicKeys().get(key);
for(KeyDerivation keypathKey : keypath.keySet()) {
//Script path change is not supported
if(key.equals(psbtOutput.getTapInternalKey()) && keypathKey.getMasterFingerprint().equals(masterFingerprint)) {
txOutput.addAllAddressN(keypathKey.getDerivation().stream().map(ChildNumber::i).toList());
txOutput.setScriptType(TrezorMessageBitcoin.OutputScriptType.PAYTOTAPROOT);
txOutput.clearAddress();
}
}
}
}
//Add multisig info
if(psbtOutput.getWitnessScript() != null || psbtOutput.getRedeemScript() != null) {
Optional<TrezorMessageBitcoin.MultisigRedeemScriptType> optMultisigRedeemScriptType = getMultisig(
psbtOutput.getWitnessScript() != null ? psbtOutput.getWitnessScript() : psbtOutput.getRedeemScript(),
psbt.getExtendedPublicKeys(), psbtOutput.getDerivedPublicKeys());
if(optMultisigRedeemScriptType.isPresent()) {
txOutput.setMultisig(optMultisigRedeemScriptType.get());
if(optWitnessType.isEmpty()) {
txOutput.setScriptType(TrezorMessageBitcoin.OutputScriptType.PAYTOMULTISIG);
}
}
}
outputs.add(txOutput.build());
}
//Prepare prev txs
Map<Sha256Hash, TrezorDevice.PrevTx> prevTxs = new LinkedHashMap<>();
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
if(psbtInput.getNonWitnessUtxo() != null) {
Transaction prev = psbtInput.getNonWitnessUtxo();
TrezorMessageBitcoin.PrevTx prevTx = TrezorMessageBitcoin.PrevTx.newBuilder()
.setVersion((int)prev.getVersion())
.setLockTime((int)prev.getLocktime())
.setInputsCount(prev.getInputs().size())
.setOutputsCount(prev.getOutputs().size()).build();
List<TrezorMessageBitcoin.PrevInput> prevInputs = new ArrayList<>();
for(TransactionInput input : prev.getInputs()) {
TrezorMessageBitcoin.PrevInput prevInput = TrezorMessageBitcoin.PrevInput.newBuilder()
.setPrevHash(ByteString.copyFrom(Utils.reverseBytes(serUInt256(input.getOutpoint().getHash().toBigInteger()))))
.setPrevIndex((int)input.getOutpoint().getIndex())
.setScriptSig(ByteString.copyFrom(input.getScriptBytes()))
.setSequence((int)input.getSequenceNumber()).build();
prevInputs.add(prevInput);
}
List<TrezorMessageBitcoin.PrevOutput> prevOutputs = new ArrayList<>();
for(TransactionOutput output : prev.getOutputs()) {
TrezorMessageBitcoin.PrevOutput prevOutput = TrezorMessageBitcoin.PrevOutput.newBuilder()
.setAmount(output.getValue())
.setScriptPubkey(ByteString.copyFrom(output.getScriptBytes())).build();
prevOutputs.add(prevOutput);
}
prevTxs.put(Sha256Hash.wrap(Utils.reverseBytes(serUInt256(prev.getTxId().toBigInteger()))), new TrezorDevice.PrevTx(prevTx, prevInputs, prevOutputs));
}
}
//Sign the transaction
List<TransactionSignature> signatures = trezorDevice.signTx(Network.get(), inputs, outputs, prevTxs,
psbt.getTransaction().getVersion(), psbt.getTransaction().getLocktime());
for(int inputIndex = 0; inputIndex < psbt.getPsbtInputs().size(); inputIndex++) {
PSBTInput psbtInput = psbt.getPsbtInputs().get(inputIndex);
if(toIgnore.contains(inputIndex)) {
continue;
}
for(ECKey pubKey : psbtInput.getDerivedPublicKeys().keySet()) {
KeyDerivation keypath = psbtInput.getDerivedPublicKeys().get(pubKey);
if(keypath.getMasterFingerprint().equals(masterFingerprint) && !psbtInput.getPartialSignatures().containsKey(pubKey)) {
psbtInput.getPartialSignatures().put(pubKey, signatures.get(inputIndex));
break;
}
}
if(psbtInput.getTapInternalKey() != null && psbtInput.getTapKeyPathSignature() == null) {
psbtInput.setTapKeyPathSignature(signatures.get(inputIndex));
}
}
p++;
}
}
return psbt;
}
private void ignoreInput(List<TrezorMessageBitcoin.TxInput> inputs, List<Integer> toIgnore, int inputIndex, TrezorMessageBitcoin.TxInput.Builder txInput) {
txInput.addAllAddressN(KeyDerivation.parsePath(ScriptType.P2WPKH.getDefaultDerivationPath() + "/0/0").stream().map(ChildNumber::i).toList());
txInput.setScriptType(TrezorMessageBitcoin.InputScriptType.SPENDWITNESS);
inputs.add(txInput.build());
toIgnore.add(inputIndex);
}
private Optional<TrezorMessageBitcoin.MultisigRedeemScriptType> getMultisig(Script script, Map<ExtendedKey, KeyDerivation> globalXpubs, Map<ECKey, KeyDerivation> derivedPublicKeys) {
if(!ScriptType.MULTISIG.isScriptType(script)) {
return Optional.empty();
}
List<TrezorMessageBitcoin.MultisigRedeemScriptType.HDNodePathType.Builder> pubkeys = new ArrayList<>();
ECKey[] keys = ScriptType.MULTISIG.getPublicKeysFromScript(script);
for(ECKey key : keys) {
TrezorMessageCommon.HDNodeType hdNodeType = TrezorMessageCommon.HDNodeType.newBuilder()
.setDepth(0)
.setFingerprint(0)
.setChildNum(0)
.setChainCode(ByteString.copyFrom(Sha256Hash.ZERO_HASH.getBytes()))
.setPublicKey(ByteString.copyFrom(key.getPubKey())).build();
pubkeys.add(TrezorMessageBitcoin.MultisigRedeemScriptType.HDNodePathType.newBuilder().setNode(hdNodeType).addAllAddressN(Collections.emptyList()));
}
for(TrezorMessageBitcoin.MultisigRedeemScriptType.HDNodePathType.Builder pubkey : pubkeys) {
KeyDerivation derivation = derivedPublicKeys.get(ECKey.fromPublicOnly(pubkey.getNode().getPublicKey().toByteArray()));
if(derivation != null) {
for(ExtendedKey xpub : globalXpubs.keySet()) {
KeyDerivation globalDerivation = globalXpubs.get(xpub);
if(globalDerivation.getMasterFingerprint().equals(derivation.getMasterFingerprint()) &&
globalDerivation.getDerivation().equals(derivation.getDerivation().subList(0, globalDerivation.getDerivation().size()))) {
List<ChildNumber> childDerivation = derivation.getDerivation().subList(globalDerivation.getDerivation().size(), derivation.getDerivation().size());
pubkey.addAllAddressN(childDerivation.stream().map(ChildNumber::i).toList());
TrezorMessageCommon.HDNodeType hdNodeType = TrezorMessageCommon.HDNodeType.newBuilder()
.setDepth(xpub.getKey().getDepth())
.setFingerprint(new BigInteger(1, xpub.getKey().getParentFingerprint()).intValue())
.setChildNum(xpub.getKeyChildNumber().i())
.setChainCode(ByteString.copyFrom(xpub.getKey().getChainCode()))
.setPublicKey(ByteString.copyFrom(xpub.getKey().getPubKey())).build();
pubkey.setNode(hdNodeType);
break;
}
}
}
}
return Optional.of(TrezorMessageBitcoin.MultisigRedeemScriptType.newBuilder()
.setM(ScriptType.MULTISIG.getThreshold(script))
.addAllSignatures(IntStream.range(0, keys.length).mapToObj(i -> ByteString.empty()).toList())
.addAllPubkeys(pubkeys.stream().map(TrezorMessageBitcoin.MultisigRedeemScriptType.HDNodePathType.Builder::build).toList()).build());
}
@Override
String signMessage(String message, String path) throws DeviceException {
try(TrezorDevice trezorDevice = new TrezorDevice(device, new PassphraseUI(passphrase))) {
checkUnlocked(trezorDevice);
TrezorMessageBitcoin.InputScriptType scriptType = TrezorMessageBitcoin.InputScriptType.SPENDADDRESS;
List<ChildNumber> keypath = KeyDerivation.parsePath(path);
keypath = keypath.subList(0, Math.min(3, keypath.size()));
if(ScriptType.P2WPKH.getDefaultDerivation().equals(keypath)) {
scriptType = TrezorMessageBitcoin.InputScriptType.SPENDWITNESS;
} else if(ScriptType.P2SH_P2WPKH.getDefaultDerivation().equals(keypath)) {
scriptType = TrezorMessageBitcoin.InputScriptType.SPENDP2SHWITNESS;
}
return trezorDevice.signMessage(Network.get(), path, message, scriptType);
}
}
@Override
String displaySinglesigAddress(String path, ScriptType scriptType) throws DeviceException {
try(TrezorDevice trezorDevice = new TrezorDevice(device, new PassphraseUI(passphrase))) {
checkUnlocked(trezorDevice);
TrezorMessageBitcoin.InputScriptType inputScriptType = switch(scriptType) {
case P2SH_P2WPKH -> TrezorMessageBitcoin.InputScriptType.SPENDP2SHWITNESS;
case P2WPKH -> TrezorMessageBitcoin.InputScriptType.SPENDWITNESS;
case P2PKH -> TrezorMessageBitcoin.InputScriptType.SPENDADDRESS;
case P2TR -> {
if(!canSignTaproot(trezorDevice)) {
throw new DeviceException("This device does not support displaying Taproot addresses");
}
yield TrezorMessageBitcoin.InputScriptType.SPENDTAPROOT;
}
default -> throw new IllegalArgumentException("Unsupported script type " + scriptType);
};
return trezorDevice.getAddress(Network.get(), path, true, null, inputScriptType, false);
}
}
@Override
String displayMultisigAddress(OutputDescriptor outputDescriptor) throws DeviceException {
try(TrezorDevice trezorDevice = new TrezorDevice(device, new PassphraseUI(passphrase))) {
checkUnlocked(trezorDevice);
List<TrezorMessageBitcoin.MultisigRedeemScriptType.HDNodePathType> pubkeys = new ArrayList<>();
for(ExtendedKey xpub : outputDescriptor.sortExtendedPubKeys(outputDescriptor.getExtendedPublicKeys())) {
List<ChildNumber> childDerivation = outputDescriptor.getChildDerivation(xpub);
TrezorMessageCommon.HDNodeType hdNodeType = TrezorMessageCommon.HDNodeType.newBuilder()
.setDepth(xpub.getKey().getDepth())
.setFingerprint(new BigInteger(1, xpub.getKey().getParentFingerprint()).intValue())
.setChildNum(xpub.getKeyChildNumber().i())
.setChainCode(ByteString.copyFrom(xpub.getKey().getChainCode()))
.setPublicKey(ByteString.copyFrom(xpub.getKey().getPubKey())).build();
pubkeys.add(TrezorMessageBitcoin.MultisigRedeemScriptType.HDNodePathType.newBuilder()
.setNode(hdNodeType)
.addAllAddressN(childDerivation.subList(1, childDerivation.size()).stream().map(ChildNumber::i).toList()).build());
}
TrezorMessageBitcoin.MultisigRedeemScriptType multisig = TrezorMessageBitcoin.MultisigRedeemScriptType.newBuilder()
.setM(outputDescriptor.getMultisigThreshold())
.addAllSignatures(IntStream.range(0, pubkeys.size()).mapToObj(i -> ByteString.empty()).toList())
.addAllPubkeys(pubkeys).build();
TrezorMessageBitcoin.InputScriptType inputScriptType = switch(outputDescriptor.getScriptType()) {
case P2SH_P2WSH -> TrezorMessageBitcoin.InputScriptType.SPENDP2SHWITNESS;
case P2WSH -> TrezorMessageBitcoin.InputScriptType.SPENDWITNESS;
case P2SH -> TrezorMessageBitcoin.InputScriptType.SPENDMULTISIG;
default -> throw new IllegalArgumentException("Unsupported script type " + outputDescriptor.getScriptType());
};
for(ExtendedKey xpub : outputDescriptor.getExtendedPublicKeys()) {
KeyDerivation keyDerivation = outputDescriptor.getKeyDerivation(xpub);
String path = outputDescriptor.getKeyDerivation(xpub).extend(KeyDerivation.parsePath(outputDescriptor.getChildDerivationPath(xpub))).getDerivationPath();
try {
return trezorDevice.getAddress(Network.get(), path, true, multisig, inputScriptType, false);
} catch(DeviceException e) {
if(masterFingerprint != null && masterFingerprint.equals(keyDerivation.getMasterFingerprint())) {
throw e;
}
}
}
throw new DeviceException("No path supplied matched device keys");
}
}
@Override
public boolean promptPin() throws DeviceException {
try(TrezorDevice trezorDevice = new TrezorDevice(device, new PassphraseUI(passphrase))) {
try {
prepareDevice(trezorDevice);
} catch(DeviceNotReadyException e) {
//ignore, expected
}
if(!trezorDevice.getFeatures().getPinProtection()) {
throw new DeviceException("This device does not need a PIN");
}
if(trezorDevice.getFeatures().getUnlocked()) {
throw new DeviceException("The PIN has already been sent to this device");
}
if(Lark.isConsoleOutput()) {
System.out.println("Use 'sendpin' to provide the number positions for the PIN as displayed on your device's screen");
System.out.println(PIN_MATRIX_DESCRIPTION);
}
TrezorMessageBitcoin.GetPublicKey getPublicKey = TrezorMessageBitcoin.GetPublicKey.newBuilder()
.addAllAddressN(KeyDerivation.parsePath("m/44'/1'/0'").stream().map(ChildNumber::i).toList())
.setCoinName(trezorDevice.getCoinName(Network.TESTNET))
.setShowDisplay(false)
.setScriptType(TrezorMessageBitcoin.InputScriptType.SPENDADDRESS)
.build();
trezorDevice.callRaw(getPublicKey);
return true;
}
}
@Override
@SuppressWarnings("deprecation")
public boolean sendPin(String pin) throws DeviceException {
if(!pin.matches("\\d+")) {
throw new IllegalArgumentException("Non-numeric PIN provided");
}
try(TrezorDevice trezorDevice = new TrezorDevice(device, new PassphraseUI(passphrase))) {
Message message = trezorDevice.callRaw(TrezorMessageCommon.PinMatrixAck.newBuilder().setPin(pin).build());
if(message instanceof TrezorMessageCommon.Failure) {
TrezorMessageManagement.Features features = trezorDevice.refreshFeatures();
if(!features.getPinProtection()) {
throw new DeviceException("This device does not need a PIN");
}
if(features.getUnlocked()) {
throw new DeviceException("The PIN has already been sent to this device");
}
return false;
} else if(message instanceof TrezorMessageCommon.PassphraseRequest) {
TrezorMessageCommon.PassphraseAck passphraseAck = TrezorMessageCommon.PassphraseAck.newBuilder()
.setPassphrase((String)trezorDevice.getUI().getPassphrase(false))
.setOnDevice(false).build();
Message resp = trezorDevice.call(passphraseAck, Message.class);
if(resp instanceof TrezorMessageCommon.Deprecated_PassphraseStateRequest) {
trezorDevice.callRaw(TrezorMessageCommon.Deprecated_PassphraseStateAck.newBuilder().build());
}
}
return true;
}
}
@Override
public boolean togglePassphrase() throws DeviceException {
try(TrezorDevice trezorDevice = new TrezorDevice(device, new PassphraseUI(passphrase))) {
checkUnlocked(trezorDevice);
try {
trezorDevice.applySettings(null, !trezorDevice.getFeatures().getPassphraseProtection(),
null, null, null, null, null, null);
} catch(DeviceException e) {
if(Lark.isConsoleOutput() && trezorDevice.getModel() == TrezorModel.KEEPKEY) {
System.out.println("Confirm the action by entering your PIN");
System.out.println("Use 'sendpin' to provide the number positions for the PIN as displayed on your device's screen");
System.out.println(PIN_MATRIX_DESCRIPTION);
}
}
return true;
}
}
@Override
public String getPath() {
StringJoiner joiner = new StringJoiner(":");
joiner.add(PATH_PREFIX);
joiner.add(String.format("%03d", busNumber));
for(int i = 0; i < portNumbers.capacity(); i++) {
joiner.add(String.format("%01x", portNumbers.get(i)));
}
return joiner.toString();
}
@Override
public HardwareType getHardwareType() {
return HardwareType.TREZOR;
}
@Override
public WalletModel getModel() {
return model;
}
@Override
public Boolean needsPinSent() {
return needsPinSent;
}
@Override
public Boolean needsPassphraseSent() {
return needsPassphraseSent;
}
@Override
public String fingerprint() {
return masterFingerprint;
}
@Override
public boolean card() {
return false;
}
@Override
public String[][] warnings() {
if(warnings.isEmpty()) {
return new String[0][];
} else {
return warnings.toArray(new String[warnings.size()][]);
}
}
@Override
public String getLabel() {
return label;
}
@Override
public String getProductModel() {
return "trezor_" + trezorModel.getName().replace(" ", "_").toLowerCase(Locale.ROOT);
}
public void setPassphrase(String passphrase) {
this.passphrase = passphrase;
}
public boolean canSignTaproot(TrezorDevice trezorDevice) {
return trezorDevice.canSignTaproot();
}
}

View File

@ -0,0 +1,11 @@
package com.sparrowwallet.lark;
public class UserRefusedException extends DeviceException {
public UserRefusedException() {
super("User refused action");
}
public UserRefusedException(String message) {
super(message);
}
}

View File

@ -0,0 +1,59 @@
package com.sparrowwallet.lark;
public class Version implements Comparable<Version> {
private final String version;
public final String get() {
return this.version;
}
public Version(String version) {
if(version == null) {
throw new IllegalArgumentException("Version can not be null");
}
if(!version.matches("[0-9]+(\\.[0-9]+)*")) {
throw new IllegalArgumentException("Invalid version format");
}
this.version = version;
}
@Override
public int compareTo(Version that) {
if(that == null) {
return 1;
}
String[] thisParts = this.get().split("\\.");
String[] thatParts = that.get().split("\\.");
int length = Math.max(thisParts.length, thatParts.length);
for(int i = 0; i < length; i++) {
int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0;
int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0;
if(thisPart < thatPart) {
return -1;
}
if(thisPart > thatPart) {
return 1;
}
}
return 0;
}
@Override
public boolean equals(Object that) {
if(this == that) {
return true;
}
if(that == null) {
return false;
}
if(this.getClass() != that.getClass()) {
return false;
}
return this.compareTo((Version) that) == 0;
}
@Override
public String toString() {
return version;
}
}

View File

@ -0,0 +1,37 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.sparrowwallet.lark.Lark;
public abstract class AbstractCommand implements Command {
@Parameter(names = { "--help", "-h" }, description = "Show this help message and exit", help = true)
public boolean help;
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
if(help) {
jCommander.usage(getName());
System.exit(0);
}
if(!EnumerateCommand.NAME.equals(getName())) {
if(args.deviceType == null && args.fingerprint == null) {
error("You must specify a device type or fingerprint for all commands except enumerate");
}
}
}
protected void success(boolean success) {
Lark.showSuccess(success);
}
protected void value(Object value) {
Lark.showValue(value);
}
protected void error(String errorMessage) {
Lark.showErrorAndExit(errorMessage);
}
protected record XpubValue(String xpub) {}
}

View File

@ -0,0 +1,17 @@
package com.sparrowwallet.lark.args;
import com.sparrowwallet.drongo.protocol.ScriptType;
public enum AddrType {
legacy(ScriptType.P2PKH), wit(ScriptType.P2WPKH), sh_wit(ScriptType.P2SH_P2WPKH), tap(ScriptType.P2TR);
private final ScriptType scriptType;
AddrType(ScriptType scriptType) {
this.scriptType = scriptType;
}
public ScriptType getScriptType() {
return scriptType;
}
}

View File

@ -0,0 +1,50 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.Parameter;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.lark.Chain;
import org.slf4j.event.Level;
public class Args {
@Parameter(names = { "--device-path", "-d" }, description = "Specify the device path of the device to connect to", defaultValueDescription = "None")
public String devicePath;
@Parameter(names = { "--device-type", "-t" }, description = "Specify the type of device that will be connected. If `--device-path` not given, the first device of this type enumerated is used.", defaultValueDescription = "None")
public String deviceType;
@Parameter(names = { "--passphrase", "--password", "-p" }, description = "Device passphrase if it has one", defaultValueDescription = "None", password = true)
public String passphrase;
@Parameter(names = { "--network", "-n" }, description = "Select network to work with")
public Network network;
@Parameter(names = { "--chain" }, description = "Legacy alternative to select network to work with. If `--network` is provided it takes priority")
public Chain chain;
@Parameter(names = { "--level", "-l" }, description = "Set log level")
public Level level;
@Parameter(names = { "--debug" }, description = "Set log level to debug. If `--level` is provided it takes priority")
public boolean debug;
@Parameter(names = { "--fingerprint", "-f" }, description = "Specify the device to connect to using the first 4 bytes of the hash160 of the master public key. It will connect to the first device that matches this fingerprint.")
public byte[] fingerprint;
@Parameter(names = { "--version" }, description = "Show program's version number and exit")
public boolean version;
@Parameter(names = { "--emulators" }, description = "Enable enumeration and detection of device emulators")
public boolean emulators;
@Parameter(names = { "--wallet-desc", "-w" }, description = "Output descriptor of the wallet (used to set the wallet name)")
public String walletDescriptor;
@Parameter(names = { "--wallet-name" }, description = "Name of the wallet. `--wallet-desc` must also be provided")
public String walletName;
@Parameter(names = { "--wallet-registration" }, description = "Registration identifier of the wallet provided in hex. `--wallet-desc` and `--wallet-name` must also be provided. For Ledger, this is the wallet policy HMAC")
public String walletRegistration;
@Parameter(names = { "--help", "-h" }, description = "Show this help message and exit", help = true)
public boolean help;
}

View File

@ -0,0 +1,9 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.sparrowwallet.lark.Lark;
public interface Command {
String getName();
void run(JCommander jCommander, Lark lark, Args args) throws Exception;
}

View File

@ -0,0 +1,72 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.lark.Lark;
@Parameters(commandDescription = "Display an address")
public class DisplayAddressCommand extends AbstractCommand {
@Parameter(names = { "--desc" }, description = "Output descriptor e.g. wpkh([00000000/84h/0h/0h]xpub.../0/0), where 00000000 must match --fingerprint and xpub can be obtained with getxpub")
public String desc;
@Parameter(names = { "--path" }, description = "The BIP 32 derivation path of the key embedded in the address")
public String path;
@Parameter(names = { "--addr-type" }, description = "The address type to display")
public AddrType addrType = AddrType.wit;
@Override
public String getName() {
return "displayaddress";
}
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
super.run(jCommander, lark, args);
if(desc == null && path == null) {
error("Either --desc or --path must be specified");
}
String address;
if(desc != null) {
OutputDescriptor outputDescriptor;
try {
outputDescriptor = OutputDescriptor.getOutputDescriptor(desc);
} catch(Exception e) {
error(e.getMessage());
return;
}
if(args.devicePath != null) {
address = lark.displayAddress(args.deviceType, args.devicePath, outputDescriptor);
} else if(args.fingerprint != null) {
address = lark.displayAddress(args.fingerprint, outputDescriptor);
} else {
address = lark.displayAddress(args.deviceType, outputDescriptor);
}
} else {
try {
path = KeyDerivation.writePath(KeyDerivation.parsePath(path));
} catch(Exception e) {
error("Invalid BIP32 path: " + path);
return;
}
if(args.devicePath != null) {
address = lark.displaySinglesigAddress(args.deviceType, args.devicePath, path, addrType.getScriptType());
} else if(args.fingerprint != null) {
address = lark.displaySinglesigAddress(args.fingerprint, path, addrType.getScriptType());
} else {
address = lark.displaySinglesigAddress(args.deviceType, path, addrType.getScriptType());
}
}
value(new AddressValue(address));
}
private record AddressValue(String address) {}
}

View File

@ -0,0 +1,28 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameters;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparrowwallet.lark.HardwareClient;
import com.sparrowwallet.lark.Lark;
import java.util.List;
@Parameters(commandDescription = "List all available devices")
public class EnumerateCommand extends AbstractCommand {
public static final String NAME = "enumerate";
@Override
public String getName() {
return NAME;
}
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
super.run(jCommander, lark, args);
List<HardwareClient> clients = lark.enumerate();
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(clients.stream().map(EnumeratedDevice::new).toList()));
}
}

View File

@ -0,0 +1,55 @@
package com.sparrowwallet.lark.args;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.sparrowwallet.lark.HardwareClient;
import java.util.Locale;
@JsonPropertyOrder({"type", "path", "model", "label", "fingerprint", "needs_pin_sent", "needs_passphrase_sent", "error"})
class EnumeratedDevice {
private final HardwareClient hardwareClient;
public EnumeratedDevice(HardwareClient hardwareClient) {
this.hardwareClient = hardwareClient;
}
public String getType() {
return hardwareClient.getType().toLowerCase(Locale.ROOT);
}
public String getModel() {
return hardwareClient.getProductModel().toLowerCase(Locale.ROOT);
}
public String getLabel() {
return hardwareClient.getLabel();
}
public String getPath() {
return hardwareClient.getPath();
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public String getFingerprint() {
return hardwareClient.fingerprint();
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonProperty("needs_pin_sent")
public Boolean isNeedsPinSent() {
return hardwareClient.needsPinSent();
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonProperty("needs_passphrase_sent")
public Boolean isNeedsPassphraseSent() {
return hardwareClient.needsPassphraseSent();
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public String getError() {
return hardwareClient.error();
}
}

View File

@ -0,0 +1,40 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.lark.Lark;
@Parameters(commandDescription = "Get the extended public key for BIP 44 standard derivation paths. Convenience function to get xpubs given the address type, account, and chain type.")
public class GetMasterXpubCommand extends AbstractCommand {
@Parameter(names = { "--addr-type" }, description = "Get the master xpub used to derive addresses for this address type")
public AddrType addrType = AddrType.wit;
@Parameter(names = { "--account" }, description = "The account number")
public int account = 0;
@Override
public String getName() {
return "getmasterxpub";
}
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
super.run(jCommander, lark, args);
String path = KeyDerivation.writePath(addrType.getScriptType().getDefaultDerivation(account));
ExtendedKey xpub;
if(args.devicePath != null) {
xpub = lark.getPubKeyAtPath(args.deviceType, args.devicePath, path);
} else if(args.fingerprint != null) {
xpub = lark.getPubKeyAtPath(args.fingerprint, path);
} else {
xpub = lark.getPubKeyAtPath(args.deviceType, path);
}
value(new XpubValue(xpub.toString()));
}
}

View File

@ -0,0 +1,45 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.lark.Lark;
import java.util.List;
@Parameters(commandDescription = "Get an extended public key derived at a BIP 32 derivation path")
public class GetXpubCommand extends AbstractCommand {
@Parameter(description = "path", required = true)
public List<String> path;
@Override
public String getName() {
return "getxpub";
}
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
super.run(jCommander, lark, args);
String xpubPath;
try {
xpubPath = KeyDerivation.writePath(KeyDerivation.parsePath(path.getFirst()));
} catch(Exception e) {
error("Invalid BIP32 path: " + path.getFirst());
return;
}
ExtendedKey xpub;
if(args.devicePath != null) {
xpub = lark.getPubKeyAtPath(args.deviceType, args.devicePath, xpubPath);
} else if(args.fingerprint != null) {
xpub = lark.getPubKeyAtPath(args.fingerprint, xpubPath);
} else {
xpub = lark.getPubKeyAtPath(args.deviceType, xpubPath);
}
value(new XpubValue(xpub.toString()));
}
}

View File

@ -0,0 +1,29 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameters;
import com.sparrowwallet.lark.Lark;
@Parameters(commandDescription = "Have the device prompt for your PIN")
public class PromptPinCommand extends AbstractCommand {
@Override
public String getName() {
return "promptpin";
}
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
super.run(jCommander, lark, args);
boolean success;
if(args.devicePath != null) {
success = lark.promptPin(args.deviceType, args.devicePath);
} else if(args.fingerprint != null) {
success = lark.promptPin(args.fingerprint);
} else {
success = lark.promptPin(args.deviceType);
}
success(success);
}
}

View File

@ -0,0 +1,35 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.sparrowwallet.lark.Lark;
import java.util.List;
@Parameters(commandDescription = "Send the numeric positions for your PIN to the device")
public class SendPinCommand extends AbstractCommand {
@Parameter(description = "pin", required = true)
public List<String> pin;
@Override
public String getName() {
return "sendpin";
}
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
super.run(jCommander, lark, args);
boolean success;
if(args.devicePath != null) {
success = lark.sendPin(args.deviceType, args.devicePath, pin.getFirst());
} else if(args.fingerprint != null) {
success = lark.sendPin(args.fingerprint, pin.getFirst());
} else {
success = lark.sendPin(args.deviceType, pin.getFirst());
}
success(success);
}
}

View File

@ -0,0 +1,47 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.lark.Lark;
import java.util.List;
@Parameters(commandDescription = "Sign a message")
public class SignMessageCommand extends AbstractCommand {
@Parameter(description = "message path", arity = 2)
public List<String> params;
@Override
public String getName() {
return "signmessage";
}
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
super.run(jCommander, lark, args);
String message = params.getFirst();
String path = params.getLast();
try {
path = KeyDerivation.writePath(KeyDerivation.parsePath(path));
} catch(Exception e) {
error("Invalid BIP32 path: " + path);
return;
}
String signature;
if(args.devicePath != null) {
signature = lark.signMessage(args.deviceType, args.devicePath, message, path);
} else if(args.fingerprint != null) {
signature = lark.signMessage(args.fingerprint, message, path);
} else {
signature = lark.signMessage(args.deviceType, message, path);
}
value(new SignatureValue(signature));
}
private record SignatureValue(String signature) {}
}

View File

@ -0,0 +1,48 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.lark.Lark;
import java.util.List;
@Parameters(commandDescription = "Sign a PSBT")
public class SignTxCommand extends AbstractCommand{
@Parameter(description = "psbt", required = true, arity = 1)
public List<String> psbt;
@Override
public String getName() {
return "signtx";
}
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
super.run(jCommander, lark, args);
String strUnsignedPsbt = psbt.getFirst();
PSBT unsignedPsbt;
try {
unsignedPsbt = PSBT.fromString(strUnsignedPsbt);
} catch(Exception e) {
error(e.getMessage());
return;
}
PSBT signedPsbt;
if(args.devicePath != null) {
signedPsbt = lark.signTransaction(args.deviceType, args.devicePath, unsignedPsbt);
} else if(args.fingerprint != null) {
signedPsbt = lark.signTransaction(args.fingerprint, unsignedPsbt);
} else {
signedPsbt = lark.signTransaction(args.deviceType, unsignedPsbt);
}
String strSignedPSBT = signedPsbt.toBase64String();
value(new PSBTValue(strSignedPSBT, !strSignedPSBT.equals(strUnsignedPsbt)));
}
private record PSBTValue(String psbt, boolean signed) {}
}

View File

@ -0,0 +1,29 @@
package com.sparrowwallet.lark.args;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameters;
import com.sparrowwallet.lark.Lark;
@Parameters(commandDescription = "Toggle BIP39 passphrase")
public class TogglePassphraseCommand extends AbstractCommand {
@Override
public String getName() {
return "togglepassphrase";
}
@Override
public void run(JCommander jCommander, Lark lark, Args args) throws Exception {
super.run(jCommander, lark, args);
boolean success;
if(args.devicePath != null) {
success = lark.togglePassphrase(args.deviceType, args.devicePath);
} else if(args.fingerprint != null) {
success = lark.togglePassphrase(args.fingerprint);
} else {
success = lark.togglePassphrase(args.deviceType);
}
success(success);
}
}

View File

@ -0,0 +1,91 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class AttestationKeys {
public static Map<Sha256Hash, AttestationPubkeyInfo> getPubKeysMap() {
return getAttestationPubkeys().stream().collect(Collectors.toMap(pubKeyInfo -> Sha256Hash.of(pubKeyInfo.getPubkey()), Function.identity(), (u, v) -> u, LinkedHashMap::new));
}
public static List<AttestationPubkeyInfo> getAttestationPubkeys() {
List<AttestationPubkeyInfo> attestationPubkeys = new ArrayList<>();
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("04074ff1273b36c24e80fe3d59e0e897a81732d3f8e9cd07e17e9fc06319cd16b25cf74255674477b3ac9cbac2d12f0dc27a662681fcbc12955b0bccdcbbdcfd01"),
null
));
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("044c53a84f41fa7301b378bb3c260fc9b2ff1cbea7a78181279a8566797a736f12cea25fa2b1c27a844392fe9b37547dc6fbd00a2676b816e7d2d3562be2a0cbbd"),
null
));
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("04e9c8dc929796aac65af5084eb54dc1ee482d5e0b5c58e2c93f243c5b70b21523324bdb78d7395317da165ef1138826c3ca3c91ca95e6f490c340cf5508a4a3ec"),
null
));
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("04c2fb05889b9dff5a9fb22a59ee1d16bfc2863f0400ddcb69566e2abe8a15fa0ba1240254ca45aa310d170e724e1310ce5f611cada76c12e3c24a926a390ca4be"),
null
));
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("04c4e82d6d1b91e7853eba96a871ad31fc62620b826b0b8acf815c03de31b792a98e05bb34d3b9e0df1040eac485f03ff8bbbf7a857ef1cf2a49a60ac084efb88f"),
null
));
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("040526f5b8348a8d55e7b1cac043ce98c55bbdb3311b4d1bb2d654281edf8aeb21f018fb027a6b08e4ddc62c919e648690722d00c6f54c668c9bd8224a1d82423a"),
Utils.hexToBytes("e8fa0bd5fc80b86b9f1ea983664df33b27f6f95855d79fb43248ee4c3d3e6be6")
));
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("0422491e19766bd96a56e3f2f3926a6c57b89209ff47bd10e523b223ff65ab9af11c0a5f62c187514f2117ce772de90f9901ee122af78e69bbc4d29eec811be8ec"),
null
));
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("049f1b7180014b6de60d41f16a3c0a37b20146585e4884960249d30f3cd68c74d04420d0cedef5719d6b1529b085ecd534fa6c1690be5eb1b3331bc57b5db224dc"),
null
));
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("04adaa011a4ced11310728abb64f09636267ce0b05782da6d3eeaf987cec7c64f279ad55327184f9e5b4a1e53089b31bcc65032dad7205325f41ed3d9fdfba1f88"),
null
));
attestationPubkeys.add(new AttestationPubkeyInfo(
Utils.hexToBytes("044a70e663d7fe5fe0d4cbbb752883e35222b8d7d7bffdaa8d591995d1252528a4e9a3e4d5220d485021728b3cdad4fccc681a6ddeea8e2f7c55b4acde8d53573d"),
null
));
return attestationPubkeys;
}
public static class AttestationPubkeyInfo {
private final byte[] pubkey;
private final byte[] acceptedBootloaderHash;
public AttestationPubkeyInfo(byte[] pubkey, byte[] acceptedBootloaderHash) {
this.pubkey = pubkey;
this.acceptedBootloaderHash = acceptedBootloaderHash;
}
public byte[] getPubkey() {
return pubkey;
}
public byte[] getAcceptedBootloaderHash() {
return acceptedBootloaderHash;
}
}
}

View File

@ -0,0 +1,210 @@
package com.sparrowwallet.lark.bitbox02;
import com.google.protobuf.InvalidProtocolBufferException;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECDSASignature;
import com.sparrowwallet.drongo.crypto.Secp256r1Key;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.lark.DeviceException;
import com.sparrowwallet.lark.Version;
import com.sparrowwallet.lark.bitbox02.generated.Bitbox02System;
import com.sparrowwallet.lark.bitbox02.generated.Btc;
import com.sparrowwallet.lark.bitbox02.generated.Hww;
import com.sparrowwallet.lark.bitbox02.generated.Mnemonic;
import org.hid4java.HidDevice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BitBox02Device implements Closeable {
private static final Logger log = LoggerFactory.getLogger(BitBox02Device.class);
public static final int BITBOX02_VID = 0x03eb;
public static final int BITBOX02_PID = 0x2403;
public static final int ERR_GENERIC = 103;
public static final int ERR_DUPLICATE_ENTRY = 107;
public static final int ERR_USER_ABORT = 104;
private static final Pattern DEVICE_VERSION = Pattern.compile("v([0-9]+\\.[0-9]+\\.[0-9]+)");
private static final Version MIN_UNSUPPORTED_BITBOX02_FIRMWARE_VERSION = new Version("10.0.0");
private final Version version;
private final BitBoxProtocol bitBoxProtocol;
public BitBox02Device(HidDevice hidDevice, TransportLayer transportLayer, BitBoxNoiseConfig bitBoxNoiseConfig) throws DeviceException {
String serialNumber = hidDevice.getSerialNumber();
hidDevice.open();
Matcher matcher = DEVICE_VERSION.matcher(serialNumber);
if(matcher.find()) {
this.version = new Version(matcher.group(1));
if(version.compareTo(MIN_UNSUPPORTED_BITBOX02_FIRMWARE_VERSION) >= 0) {
throw new DeviceException("The BitBox02's firmware version " + version + " is too new for this application.");
}
if(version.compareTo(new Version("7.0.0")) >= 0) {
this.bitBoxProtocol = new BitBoxProtocolV7(transportLayer);
} else if(version.compareTo(new Version("4.0.0")) >= 0) {
this.bitBoxProtocol = new BitBoxProtocolV4(transportLayer);
} else if(version.compareTo(new Version("3.0.0")) >= 0) {
this.bitBoxProtocol = new BitBoxProtocolV3(transportLayer);
} else if(version.compareTo(new Version("2.0.0")) >= 0) {
this.bitBoxProtocol = new BitBoxProtocolV2(transportLayer);
} else {
this.bitBoxProtocol = new BitBoxProtocolV1(transportLayer);
}
if(version.compareTo(new Version("2.0.0")) >= 0) {
bitBoxNoiseConfig.attestationCheck(performAttestation());
bitBoxProtocol.unlockQuery();
}
bitBoxProtocol.noiseConnect(bitBoxNoiseConfig);
DeviceInfo deviceInfo = getDeviceInfo();
if(!deviceInfo.initialized()) {
throw new DeviceException("The BitBox02 must be initialized first");
}
if(deviceInfo.getVersion().compareTo(new Version("9.0.0")) < 0) {
throw new DeviceException("The BitBox02 firmware must be updated to at least version 9.0.0");
}
} else {
throw new DeviceException("Could not parse version from " + serialNumber);
}
}
private boolean performAttestation() throws DeviceException {
SecureRandom secureRandom = new SecureRandom();
byte[] challenge = new byte[32];
secureRandom.nextBytes(challenge);
BitBoxProtocol.Response response = bitBoxProtocol.query(BitBoxProtocol.OP_ATTESTATION, challenge);
if(!Arrays.equals(response.status, BitBoxProtocol.RESPONSE_SUCCESS)) {
return false;
}
byte[] bootloaderHash = Arrays.copyOfRange(response.data, 0, 32);
byte[] devicePubkeyBytes = Arrays.copyOfRange(response.data, 32, 96);
byte[] certificate = Arrays.copyOfRange(response.data, 96, 160);
byte[] rootPubkeyIdentifier = Arrays.copyOfRange(response.data, 160, 192);
byte[] challengeSignature = Arrays.copyOfRange(response.data, 192, 256);
Map<Sha256Hash, AttestationKeys.AttestationPubkeyInfo> attestationPubkeyInfoMap = AttestationKeys.getPubKeysMap();
if(!attestationPubkeyInfoMap.containsKey(Sha256Hash.wrap(rootPubkeyIdentifier))) {
return false;
}
AttestationKeys.AttestationPubkeyInfo rootPubKeyInfo = attestationPubkeyInfoMap.get(Sha256Hash.wrap(rootPubkeyIdentifier));
if(rootPubKeyInfo.getAcceptedBootloaderHash() != null && !Arrays.equals(rootPubKeyInfo.getAcceptedBootloaderHash(), bootloaderHash)) {
return false;
}
int halfLength = certificate.length / 2;
byte[] r = new byte[halfLength];
byte[] s = new byte[halfLength];
System.arraycopy(certificate, 0, r, 0, halfLength);
System.arraycopy(certificate, halfLength, s, 0, halfLength);
ECDSASignature signature = new ECDSASignature(new BigInteger(1, r), new BigInteger(1, s));
if(!signature.verify(Sha256Hash.hash(Utils.concat(bootloaderHash, devicePubkeyBytes)), rootPubKeyInfo.getPubkey())) {
return false;
}
Secp256r1Key secp256r1Key = new Secp256r1Key(Utils.concat(new byte[] { 0x04 }, devicePubkeyBytes));
if(!secp256r1Key.verify(Sha256Hash.hash(challenge), challengeSignature)) {
return false;
}
return true;
}
private DeviceInfo getDeviceInfo() throws DeviceException {
Hww.Request.Builder request = Hww.Request.newBuilder();
request.setDeviceInfo(Bitbox02System.DeviceInfoRequest.newBuilder().build());
Hww.Response hwwResponse = msgQuery(request.build(), Hww.Response.ResponseCase.DEVICE_INFO);
Bitbox02System.DeviceInfoResponse deviceInfoResponse = hwwResponse.getDeviceInfo();
return new DeviceInfo(deviceInfoResponse.getName(), deviceInfoResponse.getVersion(), deviceInfoResponse.isInitialized(), deviceInfoResponse.getMnemonicPassphraseEnabled(),
deviceInfoResponse.getMonotonicIncrementsRemaining(), deviceInfoResponse.getSecurechipModel());
}
public Hww.Response msgQuery(Hww.Request request, Hww.Response.ResponseCase expectedResponse) throws DeviceException {
byte[] responseBytes = bitBoxProtocol.encryptedQuery(request.toByteArray());
try {
if(log.isDebugEnabled()) {
log.debug(request.toString());
}
Hww.Response response = Hww.Response.parseFrom(responseBytes);
if(response.hasError()) {
if(response.getError().getCode() == ERR_USER_ABORT) {
throw new BitBox02Exception(response.getError().getMessage(), response.getError().getCode());
}
throw new BitBox02Exception(response.getError().getMessage(), response.getError().getCode());
}
if(expectedResponse != null && response.getResponseCase() != expectedResponse) {
throw new DeviceException("Unexpected response: " + response.getResponseCase() + ", expected: " + expectedResponse);
}
if(log.isDebugEnabled()) {
log.debug(response.toString());
}
return response;
} catch(InvalidProtocolBufferException e) {
throw new DeviceException("Invalid protocol buffer in response", e);
}
}
public Btc.BTCResponse btcMsgQuery(Btc.BTCRequest request, Btc.BTCResponse.ResponseCase expectedResponse) throws DeviceException {
Hww.Request requestBuilder = Hww.Request.newBuilder().setBtc(request).build();
Btc.BTCResponse response = msgQuery(requestBuilder, Hww.Response.ResponseCase.BTC).getBtc();
if(expectedResponse != null && response.getResponseCase() != expectedResponse) {
throw new DeviceException("Unexpected response: " + response.getResponseCase() + ", expected: " + expectedResponse);
}
return response;
}
public void togglePassphrase() throws DeviceException {
DeviceInfo deviceInfo = getDeviceInfo();
Hww.Request requestBuilder = Hww.Request.newBuilder()
.setSetMnemonicPassphraseEnabled(Mnemonic.SetMnemonicPassphraseEnabledRequest.newBuilder()
.setEnabled(!deviceInfo.mnemnoicPassphraseEnabled()).build()).build();
msgQuery(requestBuilder, Hww.Response.ResponseCase.SUCCESS);
}
public void requireAtLeastVersion(Version version) throws DeviceException {
if(this.version.compareTo(version) < 0) {
throw new DeviceException("Update the BitBox02 firmware to at least version " + version);
}
}
public Version getVersion() {
return version;
}
@Override
public void close() {
bitBoxProtocol.close();
}
public record DeviceInfo(String name, String version, boolean initialized, boolean mnemnoicPassphraseEnabled, int monotonicIncrementsRemaining, String secureChipModel) {
public Version getVersion() {
return new Version(version.substring(1));
}
}
}

View File

@ -0,0 +1,32 @@
package com.sparrowwallet.lark.bitbox02;
public enum BitBox02Edition {
MULTI("multi", "BitBox02"), BTCONLY("btconly", "BitBox02BTC");
private final String name;
private final String productString;
BitBox02Edition(String name, String productString) {
this.name = name;
this.productString = productString;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
public static BitBox02Edition fromProductString(String productString) {
for(BitBox02Edition edition : values()) {
if(edition.productString.equals(productString)) {
return edition;
}
}
return null;
}
}

View File

@ -0,0 +1,16 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.lark.DeviceException;
public class BitBox02Exception extends DeviceException {
private final int code;
public BitBox02Exception(String message, int code) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@ -0,0 +1,31 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.lark.Platform;
import java.io.File;
import java.nio.file.Path;
public class BitBoxAppNoiseConfig extends BitBoxFileNoiseConfig {
public BitBoxAppNoiseConfig() {
super(getBitBoxAppConfigFile());
}
private static File getBitBoxAppConfigFile() {
String configHome;
Platform platform = Platform.getCurrent();
if(platform == Platform.UNIX) {
configHome = System.getenv("XDG_CONFIG_HOME");
if(configHome == null) {
configHome = System.getProperty("user.home") + "/.config";
}
} else if(platform == Platform.MACOS) {
configHome = System.getProperty("user.home") + "/Library/Application Support";
} else if(platform == Platform.WINDOWS) {
configHome = System.getenv("APPDATA");
} else {
throw new UnsupportedOperationException("Unsupported platform: " + platform);
}
return Path.of(configHome, "bitbox", "bitbox02", "bitbox02.json").toFile();
}
}

View File

@ -0,0 +1,102 @@
package com.sparrowwallet.lark.bitbox02;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparrowwallet.drongo.crypto.X25519Key;
import com.sparrowwallet.lark.DeviceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
public class BitBoxFileNoiseConfig implements BitBoxNoiseConfig {
private static final Logger log = LoggerFactory.getLogger(BitBoxAppNoiseConfig.class);
private final ObjectMapper mapper = new ObjectMapper();
protected final File config;
public BitBoxFileNoiseConfig(File config) {
this.config = config;
}
@Override
public boolean showPairing(String code, DeviceResponse response) throws DeviceException {
return response.call();
}
@Override
public void attestationCheck(boolean result) {}
@Override
public boolean containsDeviceStaticPubkey(byte[] pubkey) {
NoiseFileConfig noiseConfig = read();
String base64Pubkey = Base64.getEncoder().encodeToString(pubkey);
return noiseConfig.deviceNoiseStaticPubkeys.contains(base64Pubkey);
}
@Override
public void addDeviceStaticPubkey(byte[] pubkey) {
if(!containsDeviceStaticPubkey(pubkey)) {
NoiseFileConfig noiseConfig = read();
String base64Pubkey = Base64.getEncoder().encodeToString(pubkey);
noiseConfig.deviceNoiseStaticPubkeys.add(base64Pubkey);
write(noiseConfig);
}
}
@Override
public Optional<X25519Key> getAppStaticKey() {
NoiseFileConfig noiseConfig = read();
if(noiseConfig.appNoiseStaticKeypair == null) {
return Optional.empty();
}
return Optional.of(new X25519Key(Base64.getDecoder().decode(noiseConfig.appNoiseStaticKeypair.privateKey)));
}
@Override
public void setAppStaticKey(X25519Key x25519Key) {
NoiseFileConfig noiseConfig = read();
noiseConfig.appNoiseStaticKeypair = new AppNoiseKeypair();
noiseConfig.appNoiseStaticKeypair.publicKey = Base64.getEncoder().encodeToString(x25519Key.getRawPublicKeyBytes());
noiseConfig.appNoiseStaticKeypair.privateKey = Base64.getEncoder().encodeToString(x25519Key.getRawPrivateKeyBytes());
write(noiseConfig);
}
private NoiseFileConfig read() {
try {
String json = Files.readString(config.toPath(), StandardCharsets.UTF_8);
return mapper.readValue(json, NoiseFileConfig.class);
} catch(Exception e) {
log.error("Could not read " + config.getAbsolutePath(), e);
return new NoiseFileConfig();
}
}
private void write(NoiseFileConfig noiseConfig) {
try {
mapper.writeValue(config, noiseConfig);
} catch(Exception e) {
log.error("Could not write " + config.getAbsolutePath(), e);
}
}
public static class NoiseFileConfig {
public AppNoiseKeypair appNoiseStaticKeypair;
public List<String> deviceNoiseStaticPubkeys = new ArrayList<>();
}
public static class AppNoiseKeypair {
@JsonProperty("private")
public String privateKey;
@JsonProperty("public")
public String publicKey;
}
}

View File

@ -0,0 +1,19 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.drongo.crypto.X25519Key;
import com.sparrowwallet.lark.DeviceException;
import java.util.Optional;
public interface BitBoxNoiseConfig {
boolean showPairing(String code, DeviceResponse response) throws DeviceException;
void attestationCheck(boolean result);
boolean containsDeviceStaticPubkey(byte[] pubkey);
void addDeviceStaticPubkey(byte[] pubkey);
Optional<X25519Key> getAppStaticKey();
void setAppStaticKey(X25519Key key);
abstract class DeviceResponse {
public abstract boolean call() throws DeviceException;
}
}

View File

@ -0,0 +1,169 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.X25519Key;
import com.sparrowwallet.lark.DeviceException;
import com.sparrowwallet.lark.UserRefusedException;
import com.sparrowwallet.lark.bitbox02.noise.NamedProtocolHandshakeBuilder;
import com.sparrowwallet.lark.bitbox02.noise.NoSuchPatternException;
import com.sparrowwallet.lark.bitbox02.noise.NoiseHandshake;
import com.sparrowwallet.lark.bitbox02.noise.NoiseTransport;
import org.apache.commons.codec.binary.Base32;
import javax.crypto.AEADBadTagException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Optional;
public abstract class BitBoxProtocol {
protected static final int HWW_CMD = 0x80 + 0x40 + 0x01;
protected static final byte[] OP_ATTESTATION = new byte[] { (byte)'a' };
protected static final byte[] OP_UNLOCK = new byte[] { (byte)'u' };
protected static final byte[] OP_I_CAN_HAS_HANDSHAEK = new byte[] { (byte)'h' };
protected static final byte[] OP_HER_COMEZ_TEH_HANDSHAEK = new byte[] { (byte)'H' };
protected static final byte[] OP_I_CAN_HAS_PAIRIN_VERIFICASHUN = new byte[] { (byte)'v' };
protected static final byte[] OP_NOISE_MSG = new byte[] { (byte)'n' };
protected static final byte[] RESPONSE_SUCCESS = new byte[] { 0 };
protected static final byte[] RESPONSE_FAILURE = new byte[] { 1 };
protected final TransportLayer transportLayer;
protected NoiseTransport noiseTransport;
public BitBoxProtocol(TransportLayer transportLayer) {
this.transportLayer = transportLayer;
}
public void close() {
transportLayer.close();
}
public byte[] rawQuery(byte[] msg) throws DeviceException {
long cid = transportLayer.generateCid();
return transportLayer.query(msg, HWW_CMD, cid);
}
public Response query(byte[] cmd, byte[] msgData) throws DeviceException {
byte[] response = rawQuery(Utils.concat(cmd, msgData));
return new Response(Arrays.copyOfRange(response, 0, 1), Arrays.copyOfRange(response, 1, response.length));
}
public abstract byte[] encodeNoiseRequest(byte[] encryptedMsg);
public abstract Response decodeNoiseResponse(byte[] encryptedMsg);
public abstract Response handshakeQuery(byte[] req) throws DeviceException;
public byte[] encryptedQuery(byte[] msg) throws DeviceException {
byte[] encryptedMsg = noiseTransport.writeMessage(msg);
encryptedMsg = encodeNoiseRequest(encryptedMsg);
byte[] rawResponse = rawQuery(encryptedMsg);
Response response = decodeNoiseResponse(rawResponse);
if(!Arrays.equals(response.status, BitBoxProtocol.RESPONSE_SUCCESS)) {
throw new DeviceException("Noise communication failed.");
}
try {
return noiseTransport.readMessage(response.data);
} catch(AEADBadTagException e) {
throw new DeviceException("Unable to verify authentication tag", e);
}
}
public NoiseTransport createNoiseChannel(BitBoxNoiseConfig bitBoxNoiseConfig) throws DeviceException {
if(!Arrays.equals(rawQuery(OP_I_CAN_HAS_HANDSHAEK), RESPONSE_SUCCESS)) {
throw new DeviceException("Couldn't kick off handshake");
}
// init noise channel
try {
X25519Key appStaticKey;
Optional<X25519Key> optAppStaticKey = bitBoxNoiseConfig.getAppStaticKey();
if(optAppStaticKey.isEmpty()) {
appStaticKey = new X25519Key();
bitBoxNoiseConfig.setAppStaticKey(appStaticKey);
} else {
appStaticKey = optAppStaticKey.get();
}
final NoiseHandshake initiatorHandshake = new NamedProtocolHandshakeBuilder("Noise_XX_25519_ChaChaPoly_SHA256", NoiseHandshake.Role.INITIATOR)
.setLocalStaticKeyPair(appStaticKey.getKeyPair())
.setPrologue("Noise_XX_25519_ChaChaPoly_SHA256".getBytes(StandardCharsets.UTF_8))
.build();
Response startHandshakeReply = handshakeQuery(initiatorHandshake.writeMessage((byte[]) null));
if(!Arrays.equals(startHandshakeReply.status, BitBoxProtocol.RESPONSE_SUCCESS)) {
throw new DeviceException("Handshake process request failed.");
}
initiatorHandshake.readMessage(startHandshakeReply.data);
byte[] sendMsg = initiatorHandshake.writeMessage((byte[]) null);
Response endHandshakeReply = handshakeQuery(sendMsg);
if(!Arrays.equals(endHandshakeReply.status, BitBoxProtocol.RESPONSE_SUCCESS)) {
throw new DeviceException("Handshake conclusion failed.");
}
PublicKey publicKey = initiatorHandshake.getRemoteStaticPublicKey();
boolean pairingVerificationRequiredByHost = publicKey == null || !bitBoxNoiseConfig.containsDeviceStaticPubkey(Utils.getRawKeyBytesFromX509(publicKey));
boolean pairingVerificationRequiredByDevice = Arrays.equals(endHandshakeReply.data, new byte[]{0x01});
NoiseTransport transport = initiatorHandshake.toTransport();
if(pairingVerificationRequiredByHost || pairingVerificationRequiredByDevice) {
Base32 base32 = Base32.builder().get();
String code = base32.encodeToString(initiatorHandshake.getHash());
String displayCode = code.substring(0, 5) + " " + code.substring(5, 10) + "\n" + code.substring(10, 15) + " " + code.substring(15, 20);
if(!bitBoxNoiseConfig.showPairing(displayCode, new BitBoxNoiseConfig.DeviceResponse() {
@Override
public boolean call() throws DeviceException {
byte[] deviceReponse = rawQuery(OP_I_CAN_HAS_PAIRIN_VERIFICASHUN);
if(Arrays.equals(deviceReponse, RESPONSE_SUCCESS)) {
return true;
} else if(Arrays.equals(deviceReponse, RESPONSE_FAILURE)) {
return false;
}
throw new DeviceException("Unexpected pairing response: " + Utils.bytesToHex(deviceReponse));
}
})) {
throw new UserRefusedException("Pairing refused by user");
}
if(publicKey != null) {
bitBoxNoiseConfig.addDeviceStaticPubkey(Utils.getRawKeyBytesFromX509(publicKey));
}
}
return transport;
} catch(NoSuchPatternException | NoSuchAlgorithmException e) {
throw new DeviceException("Unsupported algorithm for handshake", e);
} catch(AEADBadTagException e) {
throw new DeviceException("Unable to verify authentication tag", e);
}
}
public void noiseConnect(BitBoxNoiseConfig bitBoxNoiseConfig) throws DeviceException {
this.noiseTransport = createNoiseChannel(bitBoxNoiseConfig);
}
public abstract void unlockQuery() throws DeviceException;
public abstract void cancelOutstandingRequest();
public static class Response {
public Response(byte[] status, byte[] data) {
this.status = status;
this.data = data;
}
public byte[] status;
public byte[] data;
}
}

View File

@ -0,0 +1,38 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.lark.DeviceException;
public class BitBoxProtocolV1 extends BitBoxProtocol {
public BitBoxProtocolV1(TransportLayer transportLayer) {
super(transportLayer);
}
@Override
public void unlockQuery() throws DeviceException {
throw new UnsupportedOperationException("unlock_query is not supported in BitBox protocol V1");
}
@Override
public byte[] encodeNoiseRequest(byte[] encryptedMsg) {
return encryptedMsg;
}
@Override
public Response decodeNoiseResponse(byte[] encryptedMsg) {
if(encryptedMsg.length == 0) {
return new Response(RESPONSE_FAILURE, new byte[0]);
}
return new Response(RESPONSE_SUCCESS, encryptedMsg);
}
@Override
public Response handshakeQuery(byte[] req) throws DeviceException {
byte[] result = rawQuery(req);
return new Response(RESPONSE_SUCCESS, result);
}
@Override
public void cancelOutstandingRequest() {
throw new RuntimeException("cancel_outstanding_request should never be called here");
}
}

View File

@ -0,0 +1,17 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.lark.DeviceException;
public class BitBoxProtocolV2 extends BitBoxProtocolV1 {
public BitBoxProtocolV2(TransportLayer transportLayer) {
super(transportLayer);
}
@Override
public void unlockQuery() throws DeviceException {
byte[] unlockData = rawQuery(OP_UNLOCK);
if(unlockData.length != 0) {
throw new DeviceException("OP_UNLOCK (V2) replied with wrong length");
}
}
}

View File

@ -0,0 +1,22 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.lark.DeviceException;
import java.util.Arrays;
public class BitBoxProtocolV3 extends BitBoxProtocolV2 {
public BitBoxProtocolV3(TransportLayer transportLayer) {
super(transportLayer);
}
@Override
public void unlockQuery() throws DeviceException {
Response response = query(OP_UNLOCK, new byte[0]);
if(response.data.length != 0) {
throw new DeviceException("OP_UNLOCK (V3) replied with wrong length");
}
if(Arrays.equals(response.status,RESPONSE_FAILURE)) {
throw new DeviceException("Unlock process aborted");
}
}
}

View File

@ -0,0 +1,14 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.drongo.Utils;
public class BitBoxProtocolV4 extends BitBoxProtocolV3 {
public BitBoxProtocolV4(TransportLayer transportLayer) {
super(transportLayer);
}
@Override
public byte[] encodeNoiseRequest(byte[] encryptedMsg) {
return Utils.concat(OP_NOISE_MSG, encryptedMsg);
}
}

View File

@ -0,0 +1,89 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.lark.DeviceException;
import java.util.Arrays;
public class BitBoxProtocolV7 extends BitBoxProtocolV4 {
private boolean cancelRequested;
public BitBoxProtocolV7(TransportLayer transportLayer) {
super(transportLayer);
}
@Override
public Response handshakeQuery(byte[] req) throws DeviceException {
return query(OP_HER_COMEZ_TEH_HANDSHAEK, req);
}
@Override
public Response decodeNoiseResponse(byte[] encryptedMsg) {
return new Response(Arrays.copyOfRange(encryptedMsg, 0, 1), Arrays.copyOfRange(encryptedMsg, 1, encryptedMsg.length));
}
@Override
public byte[] rawQuery(byte[] msg) throws DeviceException {
long cid = transportLayer.generateCid();
byte[] status;
byte[] payload;
while(true) {
byte[] response = transportLayer.query(Utils.concat(HwwRequestCode.REQ_NEW.getCode(), msg), HWW_CMD, cid);
if(response.length == 0) {
throw new DeviceException("Unexpected response of length 0 from HWW stack");
}
status = Arrays.copyOfRange(response, 0, 1);
payload = Arrays.copyOfRange(response, 1, response.length);
if(Arrays.equals(status, HwwResponseCode.RSP_BUSY.getCode())) {
if(payload.length != 0) {
throw new DeviceException("Unexpected payload of length " + payload.length + " with RSP_BUSY response");
}
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
//ignore
}
} else {
break;
}
}
if(Arrays.equals(status, HwwResponseCode.RSP_NACK.getCode())) {
throw new DeviceException("Unexpected NACK response from HWW stack");
}
//The message has been sent. If we have a retry, poll the device until we're ready.
this.cancelRequested = false;
while(Arrays.equals(status, HwwResponseCode.RSP_NOT_READY.getCode())) {
if(payload.length != 0) {
throw new DeviceException("Unexpected payload of length " + payload.length + " with RSP_NOT_READY response");
}
try {
Thread.sleep(200);
} catch(InterruptedException e) {
//ignore
}
byte[] toSend = cancelRequested ? HwwRequestCode.REQ_CANCEL.getCode() : HwwRequestCode.REQ_RETRY.getCode();
byte[] response = transportLayer.query(toSend, HWW_CMD, cid);
if(response.length == 0) {
throw new DeviceException("Unexpected response of length 0 from HWW stack");
}
status = Arrays.copyOfRange(response, 0, 1);
payload = Arrays.copyOfRange(response, 1, response.length);
if(!(Arrays.equals(status, HwwResponseCode.RSP_NOT_READY.getCode()) || Arrays.equals(status, HwwResponseCode.RSP_ACK.getCode()))) {
throw new DeviceException("Unexpected response from HWW stack during retry " + Utils.bytesToHex(status));
}
}
return payload;
}
@Override
public void cancelOutstandingRequest() {
cancelRequested = true;
}
}

View File

@ -0,0 +1,9 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.lark.DeviceException;
public class ECDSANonceException extends DeviceException {
public ECDSANonceException(String message) {
super(message);
}
}

View File

@ -0,0 +1,28 @@
package com.sparrowwallet.lark.bitbox02;
import org.hid4java.HidDevice;
public class HidPhysicalLayer implements PhysicalLayer {
private final HidDevice hidDevice;
public HidPhysicalLayer(HidDevice hidDevice) {
this.hidDevice = hidDevice;
}
@Override
public void write(byte[] bytes) {
hidDevice.write(bytes, bytes.length, (byte)0);
}
@Override
public byte[] read(int size, int timeoutMs) {
byte[] buffer = new byte[size];
hidDevice.read(buffer, timeoutMs);
return buffer;
}
@Override
public void close() {
hidDevice.close();
}
}

View File

@ -0,0 +1,15 @@
package com.sparrowwallet.lark.bitbox02;
public enum HwwRequestCode {
REQ_NEW(new byte[] {0x00}), REQ_RETRY(new byte[] { 0x01 }), REQ_CANCEL(new byte[] { 0x02 }), REQ_INFO(new byte[] { 'i' });
private final byte[] code;
HwwRequestCode(byte[] code) {
this.code = code;
}
public byte[] getCode() {
return code;
}
}

View File

@ -0,0 +1,15 @@
package com.sparrowwallet.lark.bitbox02;
public enum HwwResponseCode {
RSP_ACK(new byte[] {0x00}), RSP_NOT_READY(new byte[] { 0x01 }), RSP_BUSY(new byte[] { 0x02 }), RSP_NACK(new byte[] { 0x03 });
private final byte[] code;
HwwResponseCode(byte[] code) {
this.code = code;
}
public byte[] getCode() {
return code;
}
}

View File

@ -0,0 +1,7 @@
package com.sparrowwallet.lark.bitbox02;
public interface PhysicalLayer {
void write(byte[] bytes);
byte[] read(int size, int timeoutMs);
void close();
}

View File

@ -0,0 +1,14 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.lark.DeviceException;
public abstract class TransportLayer {
public abstract void write(byte[] bytes, int endpoint, long cid) throws DeviceException;
public abstract byte[] read(int endpoint, long cid) throws DeviceException;
public byte[] query(byte[] bytes, int endpoint, long cid) throws DeviceException {
write(bytes, endpoint, cid);
return read(endpoint, cid);
}
public abstract long generateCid();
public abstract void close();
}

View File

@ -0,0 +1,146 @@
package com.sparrowwallet.lark.bitbox02;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.lark.DeviceException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;
public class U2FHid extends TransportLayer {
private final PhysicalLayer device;
public static final int USB_REPORT_SIZE = 64;
private static final int ERR_NONE = 0x00;
private static final int ERR_INVALID_CMD = 0x01;
private static final int ERR_INVALID_PAR = 0x02;
private static final int ERR_INVALID_LEN = 0x03;
private static final int ERR_INVALID_SEQ = 0x04;
private static final int ERR_MSG_TIMEOUT = 0x05;
private static final int ERR_CHANNEL_BUSY = 0x06;
private static final int ERR_LOCK_REQUIRED = 0x0A;
private static final int ERR_INVALID_CID = 0x0B;
private static final int ERR_ENCRYPTION_FAILED = 0x7E;
private static final int ERR_OTHER = 0x7F;
private static final int PING = ((byte)0x80 | 0x01) & 0xFF;
private static final int MSG = ((byte)0x80 | 0x03) & 0xFF;
private static final int LOCK = ((byte)0x80 | 0x04) & 0xFF;
private static final int INIT = ((byte)0x80 | 0x06) & 0xFF;
private static final int WINK = ((byte)0x80 | 0x08) & 0xFF;
private static final int SYNC = ((byte)0x80 | 0x3C) & 0xFF;
private static final int ERROR = ((byte)0x80 | 0x3F) & 0xFF;
public U2FHid(PhysicalLayer device) {
this.device = device;
}
public long generateCid() {
Random random = new Random();
return random.nextLong(1, 0xFFFFFFFFL);
}
public void throwException(int errorCode) throws DeviceException {
switch(errorCode) {
case ERR_INVALID_CMD -> throw new DeviceException("Received error: invalid command");
case ERR_INVALID_LEN -> throw new DeviceException("Received error: invalid length");
case ERR_INVALID_SEQ -> throw new DeviceException("Received error: invalid sequence");
case ERR_MSG_TIMEOUT -> throw new DeviceException("Received error: message timeout");
case ERR_CHANNEL_BUSY -> throw new DeviceException("Received error: channel busy");
case ERR_LOCK_REQUIRED -> throw new DeviceException("Received error: lock required");
case ERR_INVALID_CID -> throw new DeviceException("Received error: invalid channel ID");
case ERR_ENCRYPTION_FAILED -> throw new DeviceException("Received error: encryption failed");
case ERR_OTHER -> throw new DeviceException("Received error: other");
default -> throw new DeviceException("Received error: " + errorCode);
}
}
public void write(byte[] bytes, int endpoint, long cid) throws DeviceException {
if(endpoint < 0 || endpoint > 0xFF) {
throw new DeviceException("Channel command (endpoint) is out of range '0 < endpoint <= 0xFF'");
}
if(cid < 0 || cid > 0xFFFFFFFFL) {
throw new DeviceException("Channel id is out of range '0 < cid <= 0xFFFFFFFF'");
}
int dataLen = bytes.length;
if(dataLen > 0xFFFF) {
throw new DeviceException("Data is too large 'size <= 0xFFFF'");
}
int seq = 0;
int idx = 0;
byte[] buf = new byte[0];
boolean singleEmptyWrite = (dataLen == 0);
while(idx < dataLen || singleEmptyWrite) {
if(idx == 0) {
//INIT frame
buf = Arrays.copyOfRange(bytes, idx, idx + Math.min(dataLen, USB_REPORT_SIZE - 7));
ByteBuffer buffer = ByteBuffer.allocate(USB_REPORT_SIZE);
buffer.putInt((int)cid);
buffer.put((byte)endpoint);
buffer.putShort((short)dataLen);
buffer.put(buf);
for(int i = 0; i < USB_REPORT_SIZE - 7 - buf.length; i++) {
buffer.put((byte)0xEE);
}
device.write(buffer.array());
} else {
//CONT frame
buf = Arrays.copyOfRange(bytes, idx, idx + Math.min(dataLen, USB_REPORT_SIZE - 5));
ByteBuffer buffer = ByteBuffer.allocate(USB_REPORT_SIZE);
buffer.putInt((int)cid);
buffer.put((byte)seq);
buffer.put(buf);
for(int i = 0; i < USB_REPORT_SIZE - 5 - buf.length; i++) {
buffer.put((byte)0xEE);
}
device.write(buffer.array());
seq++;
}
idx += buf.length;
singleEmptyWrite = false;
}
}
public byte[] read(int endpoint, long cid) throws DeviceException {
if(endpoint < 0 || endpoint > 0xFF) {
throw new DeviceException("Channel command (endpoint) is out of range '0 < endpoint <= 0xFF'");
}
if(cid < 0 || cid > 0xFFFFFFFFL) {
throw new DeviceException("Channel id is out of range '0 < cid <= 0xFFFFFFFF'");
}
int timeoutMs = 5000000;
byte[] buf = device.read(USB_REPORT_SIZE, timeoutMs);
if(buf.length >= 3) {
long replyCid = ((long) buf[0] & 0xFF) << 24 | ((long) buf[1] & 0xFF) << 16 | ((long) buf[2] & 0xFF) << 8 | ((long) buf[3] & 0xFF);
int replyCmd = buf[4] & 0xFF;
int dataLen = ((int) buf[5] & 0xFF) << 8 | ((int) buf[6] & 0xFF);
byte[] data = Arrays.copyOfRange(buf, 7, buf.length);
int idx = buf.length - 7;
if(replyCmd == ERROR) {
throwException(data[0]);
}
while(idx < dataLen) {
//CONT response
buf = device.read(USB_REPORT_SIZE, timeoutMs);
if(buf.length < 3) {
throw new DeviceException("Did not receive a continuation frame after 5000 seconds.");
}
data = Utils.concat(data, Arrays.copyOfRange(buf, 5, buf.length));
idx += buf.length - 5;
}
if(replyCid != cid) {
throw new DeviceException("USB channel ID mismatch " + replyCid + " != " + cid);
}
if(replyCmd != endpoint) {
throw new DeviceException("USB channel command mismatch " + replyCmd + " != " + endpoint);
}
return Arrays.copyOfRange(data, 0, dataLen);
}
throw new DeviceException("Did not read anything after 5000 seconds.");
}
public void close() {
device.close();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,647 @@
// Generated by the protocol buffer compiler. DO NOT EDIT!
// NO CHECKED-IN PROTOBUF GENCODE
// source: system.proto
// Protobuf Java Version: 4.28.3
package com.sparrowwallet.lark.bitbox02.generated;
public final class System {
private System() {}
static {
com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion(
com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC,
/* major= */ 4,
/* minor= */ 28,
/* patch= */ 3,
/* suffix= */ "",
System.class.getName());
}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistryLite registry) {
}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistry registry) {
registerAllExtensions(
(com.google.protobuf.ExtensionRegistryLite) registry);
}
public interface RebootRequestOrBuilder extends
// @@protoc_insertion_point(interface_extends:com.sparrowwallet.lark.bitbox02.generated.RebootRequest)
com.google.protobuf.MessageOrBuilder {
/**
* <code>.com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose purpose = 1;</code>
* @return The enum numeric value on the wire for purpose.
*/
int getPurposeValue();
/**
* <code>.com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose purpose = 1;</code>
* @return The purpose.
*/
com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose getPurpose();
}
/**
* Protobuf type {@code com.sparrowwallet.lark.bitbox02.generated.RebootRequest}
*/
public static final class RebootRequest extends
com.google.protobuf.GeneratedMessage implements
// @@protoc_insertion_point(message_implements:com.sparrowwallet.lark.bitbox02.generated.RebootRequest)
RebootRequestOrBuilder {
private static final long serialVersionUID = 0L;
static {
com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion(
com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC,
/* major= */ 4,
/* minor= */ 28,
/* patch= */ 3,
/* suffix= */ "",
RebootRequest.class.getName());
}
// Use RebootRequest.newBuilder() to construct.
private RebootRequest(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
super(builder);
}
private RebootRequest() {
purpose_ = 0;
}
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return com.sparrowwallet.lark.bitbox02.generated.System.internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_descriptor;
}
@java.lang.Override
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return com.sparrowwallet.lark.bitbox02.generated.System.internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_fieldAccessorTable
.ensureFieldAccessorsInitialized(
com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.class, com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Builder.class);
}
/**
* Protobuf enum {@code com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose}
*/
public enum Purpose
implements com.google.protobuf.ProtocolMessageEnum {
/**
* <code>UPGRADE = 0;</code>
*/
UPGRADE(0),
/**
* <code>SETTINGS = 1;</code>
*/
SETTINGS(1),
UNRECOGNIZED(-1),
;
static {
com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion(
com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC,
/* major= */ 4,
/* minor= */ 28,
/* patch= */ 3,
/* suffix= */ "",
Purpose.class.getName());
}
/**
* <code>UPGRADE = 0;</code>
*/
public static final int UPGRADE_VALUE = 0;
/**
* <code>SETTINGS = 1;</code>
*/
public static final int SETTINGS_VALUE = 1;
public final int getNumber() {
if (this == UNRECOGNIZED) {
throw new java.lang.IllegalArgumentException(
"Can't get the number of an unknown enum value.");
}
return value;
}
/**
* @param value The numeric wire value of the corresponding enum entry.
* @return The enum associated with the given numeric wire value.
* @deprecated Use {@link #forNumber(int)} instead.
*/
@java.lang.Deprecated
public static Purpose valueOf(int value) {
return forNumber(value);
}
/**
* @param value The numeric wire value of the corresponding enum entry.
* @return The enum associated with the given numeric wire value.
*/
public static Purpose forNumber(int value) {
switch (value) {
case 0: return UPGRADE;
case 1: return SETTINGS;
default: return null;
}
}
public static com.google.protobuf.Internal.EnumLiteMap<Purpose>
internalGetValueMap() {
return internalValueMap;
}
private static final com.google.protobuf.Internal.EnumLiteMap<
Purpose> internalValueMap =
new com.google.protobuf.Internal.EnumLiteMap<Purpose>() {
public Purpose findValueByNumber(int number) {
return Purpose.forNumber(number);
}
};
public final com.google.protobuf.Descriptors.EnumValueDescriptor
getValueDescriptor() {
if (this == UNRECOGNIZED) {
throw new java.lang.IllegalStateException(
"Can't get the descriptor of an unrecognized enum value.");
}
return getDescriptor().getValues().get(ordinal());
}
public final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptorForType() {
return getDescriptor();
}
public static final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptor() {
return com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.getDescriptor().getEnumTypes().get(0);
}
private static final Purpose[] VALUES = values();
public static Purpose valueOf(
com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
if (desc.getType() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"EnumValueDescriptor is not for this type.");
}
if (desc.getIndex() == -1) {
return UNRECOGNIZED;
}
return VALUES[desc.getIndex()];
}
private final int value;
private Purpose(int value) {
this.value = value;
}
// @@protoc_insertion_point(enum_scope:com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose)
}
public static final int PURPOSE_FIELD_NUMBER = 1;
private int purpose_ = 0;
/**
* <code>.com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose purpose = 1;</code>
* @return The enum numeric value on the wire for purpose.
*/
@java.lang.Override public int getPurposeValue() {
return purpose_;
}
/**
* <code>.com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose purpose = 1;</code>
* @return The purpose.
*/
@java.lang.Override public com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose getPurpose() {
com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose result = com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose.forNumber(purpose_);
return result == null ? com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose.UNRECOGNIZED : result;
}
private byte memoizedIsInitialized = -1;
@java.lang.Override
public final boolean isInitialized() {
byte isInitialized = memoizedIsInitialized;
if (isInitialized == 1) return true;
if (isInitialized == 0) return false;
memoizedIsInitialized = 1;
return true;
}
@java.lang.Override
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
if (purpose_ != com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose.UPGRADE.getNumber()) {
output.writeEnum(1, purpose_);
}
getUnknownFields().writeTo(output);
}
@java.lang.Override
public int getSerializedSize() {
int size = memoizedSize;
if (size != -1) return size;
size = 0;
if (purpose_ != com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose.UPGRADE.getNumber()) {
size += com.google.protobuf.CodedOutputStream
.computeEnumSize(1, purpose_);
}
size += getUnknownFields().getSerializedSize();
memoizedSize = size;
return size;
}
@java.lang.Override
public boolean equals(final java.lang.Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest)) {
return super.equals(obj);
}
com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest other = (com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest) obj;
if (purpose_ != other.purpose_) return false;
if (!getUnknownFields().equals(other.getUnknownFields())) return false;
return true;
}
@java.lang.Override
public int hashCode() {
if (memoizedHashCode != 0) {
return memoizedHashCode;
}
int hash = 41;
hash = (19 * hash) + getDescriptor().hashCode();
hash = (37 * hash) + PURPOSE_FIELD_NUMBER;
hash = (53 * hash) + purpose_;
hash = (29 * hash) + getUnknownFields().hashCode();
memoizedHashCode = hash;
return hash;
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(
java.nio.ByteBuffer data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(
java.nio.ByteBuffer data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(
com.google.protobuf.ByteString data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(
com.google.protobuf.ByteString data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(byte[] data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(
byte[] data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(java.io.InputStream input)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessage
.parseWithIOException(PARSER, input);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessage
.parseWithIOException(PARSER, input, extensionRegistry);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseDelimitedFrom(java.io.InputStream input)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessage
.parseDelimitedWithIOException(PARSER, input);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseDelimitedFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessage
.parseDelimitedWithIOException(PARSER, input, extensionRegistry);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(
com.google.protobuf.CodedInputStream input)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessage
.parseWithIOException(PARSER, input);
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest parseFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return com.google.protobuf.GeneratedMessage
.parseWithIOException(PARSER, input, extensionRegistry);
}
@java.lang.Override
public Builder newBuilderForType() { return newBuilder(); }
public static Builder newBuilder() {
return DEFAULT_INSTANCE.toBuilder();
}
public static Builder newBuilder(com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest prototype) {
return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
}
@java.lang.Override
public Builder toBuilder() {
return this == DEFAULT_INSTANCE
? new Builder() : new Builder().mergeFrom(this);
}
@java.lang.Override
protected Builder newBuilderForType(
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
/**
* Protobuf type {@code com.sparrowwallet.lark.bitbox02.generated.RebootRequest}
*/
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder> implements
// @@protoc_insertion_point(builder_implements:com.sparrowwallet.lark.bitbox02.generated.RebootRequest)
com.sparrowwallet.lark.bitbox02.generated.System.RebootRequestOrBuilder {
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return com.sparrowwallet.lark.bitbox02.generated.System.internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_descriptor;
}
@java.lang.Override
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return com.sparrowwallet.lark.bitbox02.generated.System.internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_fieldAccessorTable
.ensureFieldAccessorsInitialized(
com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.class, com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Builder.class);
}
// Construct using com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.newBuilder()
private Builder() {
}
private Builder(
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
super(parent);
}
@java.lang.Override
public Builder clear() {
super.clear();
bitField0_ = 0;
purpose_ = 0;
return this;
}
@java.lang.Override
public com.google.protobuf.Descriptors.Descriptor
getDescriptorForType() {
return com.sparrowwallet.lark.bitbox02.generated.System.internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_descriptor;
}
@java.lang.Override
public com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest getDefaultInstanceForType() {
return com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.getDefaultInstance();
}
@java.lang.Override
public com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest build() {
com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest result = buildPartial();
if (!result.isInitialized()) {
throw newUninitializedMessageException(result);
}
return result;
}
@java.lang.Override
public com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest buildPartial() {
com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest result = new com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest(this);
if (bitField0_ != 0) { buildPartial0(result); }
onBuilt();
return result;
}
private void buildPartial0(com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest result) {
int from_bitField0_ = bitField0_;
if (((from_bitField0_ & 0x00000001) != 0)) {
result.purpose_ = purpose_;
}
}
@java.lang.Override
public Builder mergeFrom(com.google.protobuf.Message other) {
if (other instanceof com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest) {
return mergeFrom((com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest)other);
} else {
super.mergeFrom(other);
return this;
}
}
public Builder mergeFrom(com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest other) {
if (other == com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.getDefaultInstance()) return this;
if (other.purpose_ != 0) {
setPurposeValue(other.getPurposeValue());
}
this.mergeUnknownFields(other.getUnknownFields());
onChanged();
return this;
}
@java.lang.Override
public final boolean isInitialized() {
return true;
}
@java.lang.Override
public Builder mergeFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
if (extensionRegistry == null) {
throw new java.lang.NullPointerException();
}
try {
boolean done = false;
while (!done) {
int tag = input.readTag();
switch (tag) {
case 0:
done = true;
break;
case 8: {
purpose_ = input.readEnum();
bitField0_ |= 0x00000001;
break;
} // case 8
default: {
if (!super.parseUnknownField(input, extensionRegistry, tag)) {
done = true; // was an endgroup tag
}
break;
} // default:
} // switch (tag)
} // while (!done)
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
throw e.unwrapIOException();
} finally {
onChanged();
} // finally
return this;
}
private int bitField0_;
private int purpose_ = 0;
/**
* <code>.com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose purpose = 1;</code>
* @return The enum numeric value on the wire for purpose.
*/
@java.lang.Override public int getPurposeValue() {
return purpose_;
}
/**
* <code>.com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose purpose = 1;</code>
* @param value The enum numeric value on the wire for purpose to set.
* @return This builder for chaining.
*/
public Builder setPurposeValue(int value) {
purpose_ = value;
bitField0_ |= 0x00000001;
onChanged();
return this;
}
/**
* <code>.com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose purpose = 1;</code>
* @return The purpose.
*/
@java.lang.Override
public com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose getPurpose() {
com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose result = com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose.forNumber(purpose_);
return result == null ? com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose.UNRECOGNIZED : result;
}
/**
* <code>.com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose purpose = 1;</code>
* @param value The purpose to set.
* @return This builder for chaining.
*/
public Builder setPurpose(com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest.Purpose value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000001;
purpose_ = value.getNumber();
onChanged();
return this;
}
/**
* <code>.com.sparrowwallet.lark.bitbox02.generated.RebootRequest.Purpose purpose = 1;</code>
* @return This builder for chaining.
*/
public Builder clearPurpose() {
bitField0_ = (bitField0_ & ~0x00000001);
purpose_ = 0;
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:com.sparrowwallet.lark.bitbox02.generated.RebootRequest)
}
// @@protoc_insertion_point(class_scope:com.sparrowwallet.lark.bitbox02.generated.RebootRequest)
private static final com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest DEFAULT_INSTANCE;
static {
DEFAULT_INSTANCE = new com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest();
}
public static com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest getDefaultInstance() {
return DEFAULT_INSTANCE;
}
private static final com.google.protobuf.Parser<RebootRequest>
PARSER = new com.google.protobuf.AbstractParser<RebootRequest>() {
@java.lang.Override
public RebootRequest parsePartialFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
Builder builder = newBuilder();
try {
builder.mergeFrom(input, extensionRegistry);
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
throw e.setUnfinishedMessage(builder.buildPartial());
} catch (com.google.protobuf.UninitializedMessageException e) {
throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial());
} catch (java.io.IOException e) {
throw new com.google.protobuf.InvalidProtocolBufferException(e)
.setUnfinishedMessage(builder.buildPartial());
}
return builder.buildPartial();
}
};
public static com.google.protobuf.Parser<RebootRequest> parser() {
return PARSER;
}
@java.lang.Override
public com.google.protobuf.Parser<RebootRequest> getParserForType() {
return PARSER;
}
@java.lang.Override
public com.sparrowwallet.lark.bitbox02.generated.System.RebootRequest getDefaultInstanceForType() {
return DEFAULT_INSTANCE;
}
}
private static final com.google.protobuf.Descriptors.Descriptor
internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_descriptor;
private static final
com.google.protobuf.GeneratedMessage.FieldAccessorTable
internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_fieldAccessorTable;
public static com.google.protobuf.Descriptors.FileDescriptor
getDescriptor() {
return descriptor;
}
private static com.google.protobuf.Descriptors.FileDescriptor
descriptor;
static {
java.lang.String[] descriptorData = {
"\n\014system.proto\022)com.sparrowwallet.lark.b" +
"itbox02.generated\"\210\001\n\rRebootRequest\022Q\n\007p" +
"urpose\030\001 \001(\0162@.com.sparrowwallet.lark.bi" +
"tbox02.generated.RebootRequest.Purpose\"$" +
"\n\007Purpose\022\013\n\007UPGRADE\020\000\022\014\n\010SETTINGS\020\001b\006pr" +
"oto3"
};
descriptor = com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] {
});
internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_descriptor =
getDescriptor().getMessageTypes().get(0);
internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_com_sparrowwallet_lark_bitbox02_generated_RebootRequest_descriptor,
new java.lang.String[] { "Purpose", });
descriptor.resolveAllFeaturesImmutable();
}
// @@protoc_insertion_point(outer_class_scope)
}

View File

@ -0,0 +1,210 @@
package com.sparrowwallet.lark.bitbox02.noise;
import com.sparrowwallet.lark.bitbox02.noise.component.NoiseCipher;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.crypto.AEADBadTagException;
import javax.crypto.ShortBufferException;
import java.nio.ByteBuffer;
import java.security.Key;
@NotThreadSafe
class CipherState {
private Key key;
private long nonce;
private final NoiseCipher cipher;
public CipherState(final NoiseCipher cipher) {
this.cipher = cipher;
}
public void setKey(final byte[] keyBytes) {
final byte[] sizedKeyBytes;
if (keyBytes.length > 32) {
sizedKeyBytes = new byte[32];
System.arraycopy(keyBytes, 0, sizedKeyBytes, 0, 32);
} else {
sizedKeyBytes = keyBytes;
}
this.key = cipher.buildKey(sizedKeyBytes);
this.nonce = 0;
}
public boolean hasKey() {
return this.key != null;
}
public ByteBuffer decrypt(@Nullable final byte[] associatedData, final ByteBuffer ciphertext)
throws AEADBadTagException {
final ByteBuffer plaintext = ByteBuffer.allocate(getPlaintextLength(ciphertext.remaining()));
try {
decrypt(associatedData, ciphertext, plaintext);
} catch (final ShortBufferException e) {
// This should never happen for a buffer we control
throw new AssertionError(e);
}
return plaintext.flip();
}
public int decrypt(@Nullable final byte[] associatedData, final ByteBuffer ciphertext, final ByteBuffer plaintext)
throws AEADBadTagException, ShortBufferException {
if (hasKey()) {
final int plaintextLength = cipher.decrypt(key, nonce, associatedData, ciphertext, plaintext);
nonce += 1;
return plaintextLength;
} else {
final int ciphertextLength = ciphertext.remaining();
plaintext.put(ciphertext);
return ciphertextLength;
}
}
public byte[] decrypt(@Nullable final byte[] associatedData, final byte[] ciphertext) throws AEADBadTagException {
final byte[] plaintext = new byte[getPlaintextLength(ciphertext.length)];
try {
decrypt(associatedData,
ciphertext,
0,
ciphertext.length,
plaintext,
0);
} catch (final ShortBufferException e) {
// This should never happen for a buffer we control
throw new AssertionError(e);
}
return plaintext;
}
public int decrypt(@Nullable final byte[] associatedData,
final byte[] ciphertext,
final int ciphertextOffset,
final int ciphertextLength,
final byte[] plaintext,
final int plaintextOffset) throws AEADBadTagException, ShortBufferException {
if (hasKey()) {
final int plaintextLength = cipher.decrypt(key,
nonce,
associatedData,
ciphertext,
ciphertextOffset,
ciphertextLength,
plaintext,plaintextOffset);
nonce += 1;
return plaintextLength;
} else {
System.arraycopy(ciphertext, ciphertextOffset, plaintext, plaintextOffset, ciphertextLength);
return ciphertextLength;
}
}
public ByteBuffer encrypt(@Nullable final byte[] associatedData, final ByteBuffer plaintext) {
final ByteBuffer ciphertext = ByteBuffer.allocate(getCiphertextLength(plaintext.remaining()));
try {
encrypt(associatedData, plaintext, ciphertext);
} catch (final ShortBufferException e) {
// This should never happen for a buffer we control
throw new AssertionError(e);
}
return ciphertext.flip();
}
public int encrypt(@Nullable final byte[] associatedData, final ByteBuffer plaintext, final ByteBuffer ciphertext) throws ShortBufferException {
if (hasKey()) {
final int ciphertextLength = cipher.encrypt(key, nonce, associatedData, plaintext, ciphertext);
nonce += 1;
return ciphertextLength;
} else {
final int plaintextLength = plaintext.remaining();
ciphertext.put(plaintext);
return plaintextLength;
}
}
public byte[] encrypt(@Nullable final byte[] associatedData, final byte[] plaintext) {
final byte[] ciphertext = new byte[getCiphertextLength(plaintext.length)];
try {
encrypt(associatedData,
plaintext,
0,
plaintext.length,
ciphertext,
0);
} catch (final ShortBufferException e) {
// This should never happen for a buffer we control
throw new AssertionError(e);
}
return ciphertext;
}
public int encrypt(@Nullable final byte[] associatedData,
final byte[] plaintext,
final int plaintextOffset,
final int plaintextLength,
final byte[] ciphertext,
final int ciphertextOffset) throws ShortBufferException {
if (hasKey()) {
final int ciphertextLength = cipher.encrypt(key,
nonce,
associatedData,
plaintext,
plaintextOffset,
plaintextLength,
ciphertext,
ciphertextOffset);
nonce += 1;
return ciphertextLength;
} else {
System.arraycopy(plaintext, plaintextOffset, ciphertext, ciphertextOffset, plaintextLength);
return plaintextLength;
}
}
public int getCiphertextLength(final int plaintextLength) {
return hasKey() ? plaintextLength + 16 : plaintextLength;
}
public int getPlaintextLength(final int ciphertextLength) {
if (hasKey()) {
if (ciphertextLength < 16) {
throw new IllegalArgumentException("Ciphertexts must be at least 16 bytes long");
}
return ciphertextLength - 16;
} else {
return ciphertextLength;
}
}
public void rekey() {
key = cipher.rekey(key);
}
NoiseCipher getCipher() {
return cipher;
}
}

View File

@ -0,0 +1,786 @@
package com.sparrowwallet.lark.bitbox02.noise;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* A handshake pattern specifies the sequential exchange of messages that comprise a Noise handshake. Callers generally
* do not need to interact directly with handshake patterns.
*/
class HandshakePattern {
private final String name;
private final MessagePattern[] preMessagePatterns;
private final MessagePattern[] handshakeMessagePatterns;
private static final Map<String, HandshakePattern> FUNDAMENTAL_PATTERNS_BY_NAME;
static {
FUNDAMENTAL_PATTERNS_BY_NAME = Stream.of(
"""
N:
<- s
...
-> e, es
""",
"""
K:
-> s
<- s
...
-> e, es, ss
""",
"""
X:
<- s
...
-> e, es, s, ss
""",
"""
NN:
-> e
<- e, ee
""",
"""
KN:
-> s
...
-> e
<- e, ee, se
""",
"""
NK:
<- s
...
-> e, es
<- e, ee
""",
"""
KK:
-> s
<- s
...
-> e, es, ss
<- e, ee, se
""",
"""
NX:
-> e
<- e, ee, s, es
""",
"""
KX:
-> s
...
-> e
<- e, ee, se, s, es
""",
"""
XN:
-> e
<- e, ee
-> s, se
""",
"""
IN:
-> e, s
<- e, ee, se
""",
"""
XK:
<- s
...
-> e, es
<- e, ee
-> s, se
""",
"""
IK:
<- s
...
-> e, es, s, ss
<- e, ee, se
""",
"""
XX:
-> e
<- e, ee, s, es
-> s, se
""",
"""
IX:
-> e, s
<- e, ee, se, s, es
""",
"""
NK1:
<- s
...
-> e
<- e, ee, es
""",
"""
NX1:
-> e
<- e, ee, s
-> es
""",
"""
X1N:
-> e
<- e, ee
-> s
<- se
""",
"""
X1K:
<- s
...
-> e, es
<- e, ee
-> s
<- se
""",
"""
XK1:
<- s
...
-> e
<- e, ee, es
-> s, se
""",
"""
X1K1:
<- s
...
-> e
<- e, ee, es
-> s
<- se
""",
"""
X1X:
-> e
<- e, ee, s, es
-> s
<- se
""",
"""
XX1:
-> e
<- e, ee, s
-> es, s, se
""",
"""
X1X1:
-> e
<- e, ee, s
-> es, s
<- se
""",
"""
K1N:
-> s
...
-> e
<- e, ee
-> se
""",
"""
K1K:
-> s
<- s
...
-> e, es
<- e, ee
-> se
""",
"""
KK1:
-> s
<- s
...
-> e
<- e, ee, se, es
""",
"""
K1K1:
-> s
<- s
...
-> e
<- e, ee, es
-> se
""",
"""
K1X:
-> s
...
-> e
<- e, ee, s, es
-> se
""",
"""
KX1:
-> s
...
-> e
<- e, ee, se, s
-> es
""",
"""
K1X1:
-> s
...
-> e
<- e, ee, s
-> se, es
""",
"""
I1N:
-> e, s
<- e, ee
-> se
""",
"""
I1K:
<- s
...
-> e, es, s
<- e, ee
-> se
""",
"""
IK1:
<- s
...
-> e, s
<- e, ee, se, es
""",
"""
I1K1:
<- s
...
-> e, s
<- e, ee, es
-> se
""",
"""
I1X:
-> e, s
<- e, ee, s, es
-> se
""",
"""
IX1:
-> e, s
<- e, ee, se, s
-> es
""",
"""
I1X1:
-> e, s
<- e, ee, s
-> se, es
""")
.map(HandshakePattern::fromString)
.collect(Collectors.toMap(HandshakePattern::getName, handshakePattern -> handshakePattern));
}
private static final Map<String, HandshakePattern> DERIVED_PATTERNS_BY_NAME = new ConcurrentHashMap<>();
private static final String PRE_MESSAGE_SEPARATOR = "...";
HandshakePattern(final String name, final MessagePattern[] preMessagePatterns, final MessagePattern[] handshakeMessagePatterns) {
this.name = name;
this.preMessagePatterns = preMessagePatterns;
this.handshakeMessagePatterns = handshakeMessagePatterns;
}
record MessagePattern(NoiseHandshake.Role sender, Token[] tokens) {
@Override
public String toString() {
final String prefix = switch (sender()) {
case INITIATOR -> " -> ";
case RESPONDER -> " <- ";
};
return prefix + Arrays.stream(tokens())
.map(token -> token.name().toLowerCase())
.collect(Collectors.joining(", "));
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final MessagePattern that = (MessagePattern) o;
return sender == that.sender && Arrays.equals(tokens, that.tokens);
}
@Override
public int hashCode() {
int result = Objects.hash(sender);
result = 31 * result + Arrays.hashCode(tokens);
return result;
}
}
enum Token {
E,
S,
EE,
ES,
SE,
SS,
PSK;
static Token fromString(final String string) {
return switch (string) {
case "e", "E" -> E;
case "s", "S" -> S;
case "ee", "EE" -> EE;
case "es", "ES" -> ES;
case "se", "SE" -> SE;
case "ss", "SS" -> SS;
case "psk", "PSK" -> PSK;
default -> throw new IllegalArgumentException("Unrecognized token: " + string);
};
}
}
/**
* Returns the name of this handshake pattern.
*
* @return the name of this handshake pattern
*/
String getName() {
return name;
}
MessagePattern[] getPreMessagePatterns() {
return preMessagePatterns;
}
MessagePattern[] getHandshakeMessagePatterns() {
return handshakeMessagePatterns;
}
/**
* Returns a {@code HandshakePattern} instance for the handshake pattern with the given name.
*
* @param name the name of the handshake pattern for which to retrieve a {@code HandshakePattern} instance
*
* @return a {@code HandshakePattern} instance for the given handshake pattern name
*
* @throws NoSuchPatternException if the given cannot be resolved to a Noise handshake pattern
*/
static HandshakePattern getInstance(final String name) throws NoSuchPatternException {
if (FUNDAMENTAL_PATTERNS_BY_NAME.containsKey(name)) {
return FUNDAMENTAL_PATTERNS_BY_NAME.get(name);
}
@Nullable final HandshakePattern derivedPattern = DERIVED_PATTERNS_BY_NAME.computeIfAbsent(name, n -> {
try {
final String fundamentalPatternName = getFundamentalPatternName(name);
@Nullable HandshakePattern handshakePattern;
if (FUNDAMENTAL_PATTERNS_BY_NAME.containsKey(fundamentalPatternName)) {
handshakePattern = FUNDAMENTAL_PATTERNS_BY_NAME.get(fundamentalPatternName);
for (final String modifier : getModifiers(name)) {
handshakePattern = handshakePattern.withModifier(modifier);
}
} else {
handshakePattern = null;
}
return handshakePattern;
} catch (final IllegalArgumentException e) {
return null;
}
});
if (derivedPattern != null) {
return derivedPattern;
}
throw new NoSuchPatternException(name);
}
static String getFundamentalPatternName(final String fullPatternName) {
final int fundamentalPatternLength = Math.toIntExact(fullPatternName.chars()
.takeWhile(c -> c == 'N' || c == 'K' || c == 'X' || c == 'I' || c == '1')
.count());
if (fundamentalPatternLength == fullPatternName.length()) {
return fullPatternName;
} else if (fundamentalPatternLength > 0) {
return fullPatternName.substring(0, fundamentalPatternLength);
}
throw new IllegalArgumentException("Invalid Noise pattern name: " + fullPatternName);
}
static List<String> getModifiers(final String fullPatternName) {
final String fundamentalPatternName = getFundamentalPatternName(fullPatternName);
if (fullPatternName.length() == fundamentalPatternName.length()) {
return Collections.emptyList();
}
return Arrays.asList(fullPatternName.substring(fundamentalPatternName.length()).split("\\+"));
}
HandshakePattern withModifier(final String modifier) {
final MessagePattern[][] modifiedMessagePatterns;
if ("fallback".equals(modifier)) {
modifiedMessagePatterns = getPatternsWithFallbackModifier();
} else if (modifier.startsWith("psk")) {
modifiedMessagePatterns = getPatternsWithPskModifier(modifier);
} else {
throw new IllegalArgumentException("Unrecognized modifier: " + modifier);
}
assert modifiedMessagePatterns.length == 2;
return new HandshakePattern(getModifiedName(modifier), modifiedMessagePatterns[0], modifiedMessagePatterns[1]);
}
private MessagePattern[][] getPatternsWithFallbackModifier() {
if (!isValidFallbackMessagePattern(handshakeMessagePatterns[0])) {
throw new IllegalStateException("Cannot generate fallback pattern; first message pattern is not a fallback-eligible message pattern");
}
final MessagePattern[] modifiedPreMessagePatterns = new MessagePattern[getPreMessagePatterns().length + 1];
final MessagePattern[] modifiedHandshakeMessagePatterns =
new MessagePattern[getHandshakeMessagePatterns().length - 1];
System.arraycopy(getPreMessagePatterns(), 0, modifiedPreMessagePatterns, 0, getPreMessagePatterns().length);
modifiedPreMessagePatterns[modifiedPreMessagePatterns.length - 1] = getHandshakeMessagePatterns()[0];
System.arraycopy(getHandshakeMessagePatterns(), 1, modifiedHandshakeMessagePatterns,
0, getHandshakeMessagePatterns().length - 1);
return new MessagePattern[][] { modifiedPreMessagePatterns, modifiedHandshakeMessagePatterns };
}
private MessagePattern[][] getPatternsWithPskModifier(final String modifier) {
final int pskIndex = Integer.parseInt(modifier.substring("psk".length()));
final MessagePattern[] modifiedPreMessagePatterns = getPreMessagePatterns().clone();
final MessagePattern[] modifiedHandshakeMessagePatterns = getHandshakeMessagePatterns().clone();
if (pskIndex == 0) {
// Insert a PSK token at the start of the first message
final Token[] originalTokens = modifiedHandshakeMessagePatterns[0].tokens();
final Token[] modifiedTokens = new Token[originalTokens.length + 1];
modifiedTokens[0] = Token.PSK;
System.arraycopy(originalTokens, 0, modifiedTokens, 1, originalTokens.length);
modifiedHandshakeMessagePatterns[0] =
new MessagePattern(modifiedHandshakeMessagePatterns[0].sender, modifiedTokens);
} else {
// Insert a PSK at the end of the N-1st message
final Token[] originalTokens = modifiedHandshakeMessagePatterns[pskIndex - 1].tokens();
final Token[] modifiedTokens = new Token[originalTokens.length + 1];
modifiedTokens[modifiedTokens.length - 1] = Token.PSK;
System.arraycopy(originalTokens, 0, modifiedTokens, 0, originalTokens.length);
modifiedHandshakeMessagePatterns[pskIndex - 1] =
new MessagePattern(modifiedHandshakeMessagePatterns[pskIndex - 1].sender, modifiedTokens);
}
return new MessagePattern[][] { modifiedPreMessagePatterns, modifiedHandshakeMessagePatterns };
}
private String getModifiedName(final String modifier) {
final String modifiedName;
if (getName().equals(getFundamentalPatternName(getName()))) {
// Our current name doesn't have any modifiers, and so this is the first
modifiedName = getName() + modifier;
} else {
modifiedName = getName() + "+" + modifier;
}
return modifiedName;
}
static boolean isValidFallbackMessagePattern(final MessagePattern messagePattern) {
if (messagePattern.sender() != NoiseHandshake.Role.INITIATOR) {
return false;
}
if (messagePattern.tokens().length == 1) {
return messagePattern.tokens()[0] == Token.E || messagePattern.tokens()[0] == Token.S;
} else if (messagePattern.tokens().length == 2) {
return messagePattern.tokens()[0] == Token.E && messagePattern.tokens()[1] == Token.S;
}
return false;
}
static HandshakePattern fromString(final String patternString) {
final String name = patternString.lines()
.findFirst()
.filter(line -> line.endsWith(":"))
.map(line -> line.substring(0, line.length() - 1))
.orElseThrow(() -> new IllegalArgumentException("Pattern string did not begin with a name line"));
final boolean hasPreMessages = patternString.lines()
.map(String::trim)
.anyMatch(PRE_MESSAGE_SEPARATOR::equals);
final MessagePattern[] preMessagePatterns;
final MessagePattern[] messagePatterns;
if (hasPreMessages) {
preMessagePatterns = patternString.lines()
// Skip the name line
.skip(1)
.map(String::trim)
.takeWhile(line -> !PRE_MESSAGE_SEPARATOR.equals(line))
.map(HandshakePattern::messagePatternFromString)
.toList()
.toArray(new MessagePattern[0]);
messagePatterns = patternString.lines()
// Skip the name line
.skip(1)
.map(String::trim)
.dropWhile(line -> !PRE_MESSAGE_SEPARATOR.equals(line))
// Skip the separator itself
.skip(1)
.map(HandshakePattern::messagePatternFromString)
.toList()
.toArray(new MessagePattern[0]);
} else {
preMessagePatterns = new MessagePattern[0];
messagePatterns = patternString.lines()
// Skip the name line
.skip(1)
.map(String::trim)
.map(HandshakePattern::messagePatternFromString)
.toList()
.toArray(new MessagePattern[0]);
}
return new HandshakePattern(name, preMessagePatterns, messagePatterns);
}
private static MessagePattern messagePatternFromString(final String messagePatternString) {
final NoiseHandshake.Role sender;
if (messagePatternString.startsWith("-> ")) {
sender = NoiseHandshake.Role.INITIATOR;
} else if (messagePatternString.startsWith("<- ")) {
sender = NoiseHandshake.Role.RESPONDER;
} else {
throw new IllegalArgumentException("Could not identify sender");
}
final Token[] tokens = Arrays.stream(messagePatternString.substring(3).split(","))
.map(String::trim)
.map(Token::fromString)
.toList()
.toArray(new Token[0]);
return new MessagePattern(sender, tokens);
}
/**
* Checks whether this is a one-way handshake pattern.
*
* @return {@code true} if this is a one-way handshake pattern or {@code false} if it is an interactive handshake
* pattern
*
* @see <a href="https://noiseprotocol.org/noise.html#one-way-handshake-patterns">The Noise Protocol Framework - One-way handshake patterns</a>
*/
boolean isOneWayPattern() {
return Arrays.stream(getHandshakeMessagePatterns())
.allMatch(messagePattern -> messagePattern.sender() == NoiseHandshake.Role.INITIATOR);
}
boolean isFallbackPattern() {
return getModifiers(getName()).contains("fallback");
}
boolean isPreSharedKeyHandshake() {
return Arrays.stream(getHandshakeMessagePatterns())
.flatMap(messagePattern -> Arrays.stream(messagePattern.tokens()))
.anyMatch(token -> token == Token.PSK);
}
/**
* Returns the number of pre-shared keys either party in this handshake must provide prior to beginning the handshake.
*
* @return the number of pre-shared keys either party in this handshake must provide prior to beginning the handshake
*/
int getRequiredPreSharedKeyCount() {
return Math.toIntExact(Arrays.stream(getHandshakeMessagePatterns())
.flatMap(messagePattern -> Arrays.stream(messagePattern.tokens()))
.filter(token -> token == Token.PSK)
.count());
}
/**
* Checks whether the party with the given role in this handshake must supply a local static key pair prior to
* beginning the handshake.
*
* @param role the role of the party in this handshake
*
* @return {@code true} if the given party must provide a local static key pair prior to beginning the handshake or
* {@code false} otherwise
*/
boolean requiresLocalStaticKeyPair(final NoiseHandshake.Role role) {
// The given role needs a local static key pair if any pre-handshake message or handshake message involves that role
// sending a static key to the other party
return Stream.concat(Arrays.stream(getPreMessagePatterns()), Arrays.stream(getHandshakeMessagePatterns()))
.filter(messagePattern -> messagePattern.sender() == role)
.flatMap(messagePattern -> Arrays.stream(messagePattern.tokens()))
.anyMatch(token -> token == Token.S);
}
/**
* Checks whether the party with the given role in this handshake must supply a remote ephemeral public key prior to
* beginning the handshake.
*
* @param role the role of the party in this handshake
*
* @return {@code true} if the given party must provide a remote ephemeral public key prior to beginning the handshake
* or {@code false} otherwise
*/
boolean requiresRemoteEphemeralPublicKey(final NoiseHandshake.Role role) {
// The given role needs a remote static key pair if the handshake pattern involves that role receiving an ephemeral
// key from the other party in a pre-handshake message
return Arrays.stream(getPreMessagePatterns())
.filter(messagePattern -> messagePattern.sender() != role)
.flatMap(messagePattern -> Arrays.stream(messagePattern.tokens()))
.anyMatch(token -> token == Token.E);
}
/**
* Checks whether the party with the given role in this handshake must supply a remote static public key prior to
* beginning the handshake.
*
* @param role the role of the party in this handshake
*
* @return {@code true} if the given party must provide a remote static public key prior to beginning the handshake or
* {@code false} otherwise
*/
boolean requiresRemoteStaticPublicKey(final NoiseHandshake.Role role) {
// The given role needs a remote static key pair if the handshake pattern involves that role receiving a static key
// from the other party in a pre-handshake message
return Arrays.stream(getPreMessagePatterns())
.filter(messagePattern -> messagePattern.sender() != role)
.flatMap(messagePattern -> Arrays.stream(messagePattern.tokens()))
.anyMatch(token -> token == Token.S);
}
@Override
public String toString() {
final StringBuilder stringBuilder = new StringBuilder(getName() + ":\n");
// We know we can't end on a pre-message pattern line, so we can unconditionally append newlines after each
// pre-handshake message
Arrays.stream(getPreMessagePatterns())
.forEach(preMessagePattern -> {
stringBuilder.append(preMessagePattern);
stringBuilder.append('\n');
});
if (getPreMessagePatterns().length > 0) {
stringBuilder.append(" ");
stringBuilder.append(PRE_MESSAGE_SEPARATOR);
stringBuilder.append('\n');
}
stringBuilder.append(Arrays.stream(getHandshakeMessagePatterns())
.map(MessagePattern::toString)
.collect(Collectors.joining("\n")));
return stringBuilder.toString();
}
/**
* Tests whether this handshake pattern is equal to another object. This handshake pattern is equal to the given
* object if the given object is also a handshake pattern and has the same name and message patterns as this handshake
* pattern.
*
* @param o the other object with which to check equality
*
* @return {@code true} if this handshake pattern is equal to the given object or {@code false} otherwise
*/
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
} else if (o instanceof final HandshakePattern that) {
return Objects.equals(name, that.name)
&& Arrays.equals(preMessagePatterns, that.preMessagePatterns)
&& Arrays.equals(handshakeMessagePatterns, that.handshakeMessagePatterns);
} else {
return false;
}
}
/**
* Returns a hash code value for this handshake pattern.
*
* @return a hash code value for this handshake pattern
*/
@Override
public int hashCode() {
int result = Objects.hash(name);
result = 31 * result + Arrays.hashCode(preMessagePatterns);
result = 31 * result + Arrays.hashCode(handshakeMessagePatterns);
return result;
}
}

View File

@ -0,0 +1,215 @@
package com.sparrowwallet.lark.bitbox02.noise;
import com.sparrowwallet.lark.bitbox02.noise.component.*;
import javax.annotation.Nullable;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.util.List;
import java.util.Objects;
/**
* <p>A {@code NamedProtocolHandshakeBuilder} constructs {@link NoiseHandshake} instances given a full Noise protocol
* name and a role (initiator or responder). In contrast to {@link NoiseHandshakeBuilder}, callers are responsible for
* identifying and providing all required key material, which may vary with handshake pattern and role. For example, the
* NN handshake pattern is defined as:</p>
*
* <pre>NN:
* -&gt; e
* &lt;- e, ee</pre>
*
* <p>and so neither the initiator nor the responder requires any static or pre-shared keys:</p>
*
* {@snippet file="NamedProtocolHandshakeBuilderExample.java" region="nn-handshake"}
*
* <p>By contrast, the IK handshake pattern is defined as:</p>
*
* <pre>IK:
* &lt;- s
* ...
* -&gt; e, es, s, ss
* &lt;- e, ee, se</pre>
*
* <p>and so the initiator needs a local static key pair and a remote static public key, while the responder needs only
* a local static key pair:</p>
*
* {@snippet file="NamedProtocolHandshakeBuilderExample.java" region="ik-handshake"}
*
* @see NoiseHandshakeBuilder
*/
public class NamedProtocolHandshakeBuilder {
private final HandshakePattern handshakePattern;
private final NoiseKeyAgreement keyAgreement;
private final NoiseCipher cipher;
private final NoiseHash hash;
private final NoiseHandshake.Role role;
@Nullable private KeyPair localEphemeralKeyPair;
@Nullable private KeyPair localStaticKeyPair;
@Nullable private PublicKey remoteStaticPublicKey;
@Nullable private List<byte[]> preSharedKeys;
@Nullable private byte[] prologue;
/**
* Constructs a new Noise handshake for the given Noise protocol name and role.
*
* @param noiseProtocolName the full Noise protocol name for which to construct a handshake object
* @param role the role for the handshake object
*
* @throws NoSuchAlgorithmException if one or more components of the Noise protocol was not recognized or is not
* supported in the current JVM
* @throws NoSuchPatternException if the handshake pattern in the Noise protocol name was not recognized or is invalid
*/
public NamedProtocolHandshakeBuilder(final String noiseProtocolName, final NoiseHandshake.Role role)
throws NoSuchAlgorithmException, NoSuchPatternException {
final String[] components = noiseProtocolName.split("_");
if (components.length != 5) {
throw new IllegalArgumentException("Invalid Noise protocol name; did not contain five sections");
}
if (!"Noise".equals(components[0])) {
throw new IllegalArgumentException("Noise protocol names must begin with \"Noise_\"");
}
this.handshakePattern = HandshakePattern.getInstance(components[1]);
this.keyAgreement = NoiseKeyAgreement.getInstance(components[2]);
this.cipher = NoiseCipher.getInstance(components[3]);
this.hash = NoiseHash.getInstance(components[4]);
this.role = role;
}
/**
* Sets the prologue for this handshake.
*
* @param prologue the prologue for this handshake; may be {@code null}
*
* @return a reference to this handshake builder
*/
public NamedProtocolHandshakeBuilder setPrologue(@Nullable final byte[] prologue) {
this.prologue = prologue;
return this;
}
NamedProtocolHandshakeBuilder setLocalEphemeralKeyPair(@Nullable final KeyPair localEphemeralKeyPair) {
this.localEphemeralKeyPair = localEphemeralKeyPair;
return this;
}
/**
* Sets the local static key pair for this handshake.
*
* @param localStaticKeyPair the local static key pair for this handshake; must not be {@code null}
*
* @return a reference to this handshake builder
*
* @throws IllegalStateException if the chosen handshake pattern does not allow for local static keys
*
* @see HandshakePattern#requiresLocalStaticKeyPair(NoiseHandshake.Role)
*/
public NamedProtocolHandshakeBuilder setLocalStaticKeyPair(final KeyPair localStaticKeyPair) {
if (!handshakePattern.requiresLocalStaticKeyPair(role)) {
throw new IllegalStateException(handshakePattern.getName() + " handshake pattern does not allow local static keys for " + role + " role");
}
this.localStaticKeyPair = Objects.requireNonNull(localStaticKeyPair, "If set, local static key pair may not be null");
return this;
}
/**
* Sets the remote static public key for this handshake.
*
* @param remoteStaticPublicKey the remote static public key for this handshake; must not be {@code null}
*
* @return a reference to this builder
*
* @throws IllegalStateException if the chosen handshake pattern does not allow for remote static keys
*
* @see HandshakePattern#requiresRemoteStaticPublicKey(NoiseHandshake.Role)
*/
public NamedProtocolHandshakeBuilder setRemoteStaticPublicKey(final PublicKey remoteStaticPublicKey) {
if (!handshakePattern.requiresRemoteStaticPublicKey(role)) {
throw new IllegalStateException(handshakePattern.getName() + " handshake pattern does not allow remote static key for " + role + " role");
}
this.remoteStaticPublicKey = Objects.requireNonNull(remoteStaticPublicKey, "If set, remote static public key may not be null");
return this;
}
/**
* Sets the pre-shared keys for this handshake.
*
* @param preSharedKeys the pre-shared keys for this handshake; must not be {@code null}
*
* @return a reference to this builder
*
* @throws IllegalStateException if the chosen handshake pattern does not allow for pre-shared keys
* @throws IllegalArgumentException if the given list of pre-shared keys has a length that does not match the number
* of pre-shared keys required by the chosen handshake pattern or if any key is not exactly 32 bytes in length
*
* @see HandshakePattern#getRequiredPreSharedKeyCount()
*/
public NamedProtocolHandshakeBuilder setPreSharedKeys(final List<byte[]> preSharedKeys) {
final int requiredPreSharedKeys = handshakePattern.getRequiredPreSharedKeyCount();
if (requiredPreSharedKeys == 0) {
throw new IllegalStateException(handshakePattern.getName() + " handshake pattern does not allow pre-shared keys");
}
if (preSharedKeys.size() != requiredPreSharedKeys) {
throw new IllegalArgumentException(handshakePattern.getName() + " requires exactly " + requiredPreSharedKeys + " pre-shared keys");
}
if (preSharedKeys.stream().anyMatch(preSharedKey -> preSharedKey.length != 32)) {
throw new IllegalArgumentException("Pre-shared keys must be exactly 32 bytes");
}
this.preSharedKeys = preSharedKeys;
return this;
}
/**
* Constructs a Noise handshake with the previously-configured protocol and keys.
*
* @return a Noise handshake with the previously-configured protocol and keys
*
* @throws IllegalStateException if any keys required by the chosen handshake pattern have not been set
*
* @see HandshakePattern#requiresLocalStaticKeyPair(NoiseHandshake.Role)
* @see HandshakePattern#requiresRemoteStaticPublicKey(NoiseHandshake.Role)
* @see HandshakePattern#getRequiredPreSharedKeyCount()
*/
public NoiseHandshake build() {
if (handshakePattern.requiresRemoteStaticPublicKey(role) && remoteStaticPublicKey == null) {
throw new IllegalStateException(handshakePattern.getName() + " handshake pattern requires a remote static public key for the " + role + " role");
}
if (handshakePattern.requiresLocalStaticKeyPair(role) && localStaticKeyPair == null) {
throw new IllegalStateException(handshakePattern.getName() + " handshake pattern requires a local static key pair for the " + role + " role");
}
final int requiredPreSharedKeyCount = handshakePattern.getRequiredPreSharedKeyCount();
if (requiredPreSharedKeyCount > 0 && (preSharedKeys == null || preSharedKeys.size() != requiredPreSharedKeyCount)) {
throw new IllegalStateException(handshakePattern.getName() + " handshake pattern requires " + requiredPreSharedKeyCount + " pre-shared keys");
}
return new NoiseHandshake(role,
handshakePattern,
keyAgreement,
cipher,
hash,
prologue,
localStaticKeyPair,
localEphemeralKeyPair,
remoteStaticPublicKey,
null,
preSharedKeys);
}
}

View File

@ -0,0 +1,17 @@
package com.sparrowwallet.lark.bitbox02.noise;
/**
* Indicates that a named pattern is not a recognized fundamental or deferred Noise handshake pattern and cannot be
* derived by modifying a recognized fundamental or deferred Noise handshake pattern.
*/
public class NoSuchPatternException extends Exception {
/**
* Constructs a new "no such pattern" exception.
*
* @param patternName the name of the requested handshake pattern
*/
public NoSuchPatternException(final String patternName) {
super("No such handshake pattern: " + patternName);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
package com.sparrowwallet.lark.bitbox02.noise;
/**
* <p>A Noise transport is an interactive reader and writer of Noise transport messages. In the terminology of the Noise
* Protocol Framework specification, a {@code NoiseTransport} instance encapsulates the two "cipher states" produced by
* "splitting" a {@link NoiseHandshake}.</p>
*
* <p>Noise transport instances are stateful and are <em>not</em> thread-safe.</p>
*
* @see NoiseHandshake#toTransport()
*/
public interface NoiseTransport extends NoiseTransportReader, NoiseTransportWriter {
}

View File

@ -0,0 +1,136 @@
package com.sparrowwallet.lark.bitbox02.noise;
import javax.crypto.AEADBadTagException;
import javax.crypto.ShortBufferException;
import java.nio.ByteBuffer;
class NoiseTransportImpl implements NoiseTransport {
private final CipherState readerState;
private final CipherState writerState;
static final int MAX_NOISE_MESSAGE_SIZE = 65_535;
NoiseTransportImpl(final CipherState readerState, final CipherState writerState) {
this.readerState = readerState;
this.writerState = writerState;
}
@Override
public int getPlaintextLength(final int ciphertextLength) {
return readerState.getPlaintextLength(ciphertextLength);
}
@Override
public int getCiphertextLength(final int plaintextLength) {
return writerState.getCiphertextLength(plaintextLength);
}
@Override
public ByteBuffer readMessage(final ByteBuffer ciphertext) throws AEADBadTagException {
checkInboundMessageSize(ciphertext.remaining());
return readerState.decrypt(null, ciphertext);
}
@Override
public int readMessage(final ByteBuffer ciphertext, final ByteBuffer plaintext) throws ShortBufferException, AEADBadTagException {
checkInboundMessageSize(ciphertext.remaining());
if (plaintext.remaining() < getPlaintextLength(ciphertext.remaining())) {
throw new ShortBufferException("Plaintext buffer does not have enough remaining capacity to hold plaintext");
}
return readerState.decrypt(null, ciphertext, plaintext);
}
@Override
public byte[] readMessage(final byte[] ciphertext) throws AEADBadTagException {
checkInboundMessageSize(ciphertext.length);
return readerState.decrypt(null, ciphertext);
}
@Override
public int readMessage(final byte[] ciphertext,
final int ciphertextOffset,
final int ciphertextLength,
final byte[] plaintext,
final int plaintextOffset) throws ShortBufferException, AEADBadTagException {
checkInboundMessageSize(ciphertextLength);
if (plaintext.length - plaintextOffset < getPlaintextLength(ciphertextLength)) {
throw new ShortBufferException("Plaintext array after offset is not large enough to hold plaintext");
}
return readerState.decrypt(null,
ciphertext, ciphertextOffset, ciphertextLength,
plaintext, plaintextOffset);
}
private void checkInboundMessageSize(final int ciphertextLength) {
if (ciphertextLength > MAX_NOISE_MESSAGE_SIZE) {
throw new IllegalArgumentException("Message is larger than maximum allowed Noise transport message size");
}
}
@Override
public ByteBuffer writeMessage(final ByteBuffer plaintext) {
checkOutboundMessageSize(plaintext.remaining());
return writerState.encrypt(null, plaintext);
}
@Override
public int writeMessage(final ByteBuffer plaintext, final ByteBuffer ciphertext) throws ShortBufferException {
checkOutboundMessageSize(plaintext.remaining());
if (ciphertext.remaining() < getCiphertextLength(plaintext.remaining())) {
throw new ShortBufferException("Ciphertext buffer does not have enough remaining capacity to hold ciphertext");
}
return writerState.encrypt(null, plaintext, ciphertext);
}
@Override
public byte[] writeMessage(final byte[] plaintext) {
checkOutboundMessageSize(plaintext.length);
return writerState.encrypt(null, plaintext);
}
@Override
public int writeMessage(final byte[] plaintext,
final int plaintextOffset,
final int plaintextLength,
final byte[] ciphertext,
final int ciphertextOffset) throws ShortBufferException {
checkOutboundMessageSize(plaintextLength);
if (ciphertext.length - ciphertextOffset < getCiphertextLength(plaintextLength)) {
throw new ShortBufferException("Ciphertext array after offset is not large enough to hold ciphertext");
}
return writerState.encrypt(null,
plaintext, plaintextOffset, plaintextLength,
ciphertext, ciphertextOffset);
}
void checkOutboundMessageSize(final int plaintextLength) {
if (getCiphertextLength(plaintextLength) > MAX_NOISE_MESSAGE_SIZE) {
throw new IllegalArgumentException("Ciphertext would be larger than maximum allowed Noise transport message size");
}
}
@Override
public void rekeyReader() {
readerState.rekey();
}
@Override
public void rekeyWriter() {
writerState.rekey();
}
}

View File

@ -0,0 +1,119 @@
package com.sparrowwallet.lark.bitbox02.noise;
import javax.crypto.AEADBadTagException;
import javax.crypto.ShortBufferException;
import java.nio.ByteBuffer;
/**
* <p>A Noise transport reader decrypts Noise transport messages. In the terminology of the Noise
* Protocol Framework specification, a {@code NoiseTransportReader} instance encapsulates a "cipher state" produced by
* "splitting" a {@link NoiseHandshake} instance.</p>
*
* <p>Noise transport reader instances are stateful and are <em>not</em> thread-safe.</p>
*
* @see NoiseHandshake#toTransportReader()
* @see NoiseHandshake#toTransport()
*/
public interface NoiseTransportReader {
/**
* Returns the length of the plaintext resulting from the decryption of a ciphertext of the given size.
*
* @param ciphertextLength the length of a ciphertext
*
* @return the length of the plaintext resulting from the decryption of a ciphertext of the given size
*
* @throws IllegalArgumentException if the given ciphertext length is too small to contain a valid AEAD tag
*/
int getPlaintextLength(final int ciphertextLength);
/**
* Decrypts a Noise transport message and verifies its AEAD tag. This method returns a new {@link ByteBuffer} sized exactly
* to contain the resulting plaintext. The returned buffer's position will be zero, and its limit and capacity will be
* equal to the plaintext length.
* <p>
* All {@code ciphertext.remaining()} bytes starting at {@code ciphertext.position()} are processed. Upon return, the
* ciphertext buffer's position will be equal to its limit; its limit will not have changed.
*
* @param ciphertext the ciphertext of the Noise transport message to decrypt
*
* @return a {@code ByteBuffer} containing the resulting plaintext
*
* @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value
* @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag or if it is
* larger than the maximum allowed Noise transport message size
*
* @see #getPlaintextLength(int)
*/
ByteBuffer readMessage(final ByteBuffer ciphertext) throws AEADBadTagException;
/**
* Decrypts a Noise transport message and verifies its AEAD tag. This method writes the resulting plaintext into the given
* {@code plaintext} buffer. Callers are responsible for ensuring that the given plaintext buffer has enough remaining
* capacity to hold the resulting plaintext.
* <p>
* All {@code ciphertext.remaining()} bytes starting at {@code ciphertext.position()} are processed. Upon return, the
* ciphertext buffer's position will be equal to its limit; its limit will not have changed. The plaintext buffer's
* position will have advanced by n, where n is the value returned by this method; the plaintext buffer's limit will
* not have changed.
*
* @param ciphertext the ciphertext of the Noise transport message to decrypt
* @param plaintext the buffer into which to write the resulting plaintext
*
* @return the number of bytes written into the {@code plaintext} buffer
*
* @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value
* @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag or if it is
* larger than the maximum allowed Noise transport message size
* @throws ShortBufferException if the given plaintext buffer does not have enough remaining capacity to hold the
* resulting plaintext
*
* @see #getPlaintextLength(int)
*/
int readMessage(final ByteBuffer ciphertext, final ByteBuffer plaintext) throws ShortBufferException, AEADBadTagException;
/**
* Decrypts a Noise transport message, returning a new byte array sized exactly to contain the resulting plaintext.
*
* @param ciphertext the ciphertext of the Noise transport message to decrypt
*
* @return a new byte array containing the resulting plaintext
*
* @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated AEAD tag
* @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag or if it is
* larger than the maximum allowed Noise transport message size
*/
byte[] readMessage(final byte[] ciphertext) throws AEADBadTagException;
/**
* Decrypts a Noise transport message and writes the resulting plaintext into the given byte array. Note that
* {@code ciphertext} and {@code plaintext} may refer to the same byte array, allowing for in-place decryption.
*
* @param ciphertext the ciphertext of the Noise transport message to decrypt
* @param ciphertextOffset the position within {@code ciphertext} at which to begin reading the ciphertext and AEAD
* tag
* @param ciphertextLength the length of the ciphertext and AEAD tag within {@code ciphertext}
* @param plaintext a byte array into which to write the decrypted plaintext
* @param plaintextOffset the offset within {@code plaintext} where the plaintext begins
*
* @return the number of bytes written to {@code plaintext}
*
* @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value
* @throws ShortBufferException if {@code plaintext} is not long enough (after its offset) to contain the resulting
* plaintext
* @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag or if it is
* larger than the maximum allowed Noise transport message size
*/
int readMessage(final byte[] ciphertext,
final int ciphertextOffset,
final int ciphertextLength,
final byte[] plaintext,
final int plaintextOffset) throws ShortBufferException, AEADBadTagException;
/**
* Sets the decryption key used by this reader to a new pseudo-random key derived from the current key. This operation
* must be coordinated with the sending party, otherwise messages from the sending party will be unintelligible and
* decrypting future messages will fail.
*/
void rekeyReader();
}

View File

@ -0,0 +1,119 @@
package com.sparrowwallet.lark.bitbox02.noise;
import javax.crypto.ShortBufferException;
import java.nio.ByteBuffer;
/**
* <p>A Noise transport writer encrypts Noise transport messages. In the terminology of the Noise Protocol Framework
* specification, a {@code NoiseTransportWriter} instance encapsulates a "cipher state" produced by "splitting" a
* {@link NoiseHandshake} instance.</p>
*
* <p>Noise transport writer instances are stateful and are <em>not</em> thread-safe.</p>
*
* @see NoiseHandshake#toTransportWriter()
* @see NoiseHandshake#toTransport()
*/
public interface NoiseTransportWriter {
/**
* Returns the length of the ciphertext resulting from the encryption of a plaintext of the given length.
*
* @param plaintextLength the length of a plaintext
*
* @return the length of the ciphertext resulting from the encryption of a plaintext of the given size
*/
int getCiphertextLength(final int plaintextLength);
/**
* Encrypts a Noise transport message, returning a new byte buffer sized exactly to contain the resulting ciphertext.
* <p>
* All {@code plaintext.remaining()} bytes starting at {@code plaintext.position()} are processed. Upon return, the
* plaintext buffer's position will be equal to its limit; its limit will not have changed. The returned ciphertext
* buffer's position will be zero, and its limit will be equal to its capacity.
*
* @param plaintext the plaintext to encrypt
*
* @return a new byte buffer containing the resulting ciphertext and AEAD tag
*
* @throws IllegalArgumentException if the ciphertext for the given plaintext would be larger than the maximum allowed
* Noise transport message size
*
* @see #getCiphertextLength(int)
*/
ByteBuffer writeMessage(final ByteBuffer plaintext);
/**
* Encrypts a Noise transport message. Callers are responsible for ensuring that the given ciphertext buffer has
* enough remaining capacity to hold the resulting ciphertext and AEAD tag.
* <p>
* All {@code plaintext.remaining()} bytes starting at {@code plaintext.position()} are processed. Upon return, the
* plaintext buffer's position will be equal to its limit; its limit will not have changed. The ciphertext buffer's
* position will have advanced by n, where n is the value returned by this method; the ciphertext buffer's limit will
* not have changed.
* <p>
* Note that the ciphertext and plaintext buffers must be different, but may refer to the same underlying byte array
* to facilitate in-place encryption.
*
* @param plaintext the plaintext to encrypt
* @param ciphertext the buffer into which to write the resulting ciphertext and AEAD tag
*
* @return the number of bytes written into the ciphertext buffer
*
* @throws IllegalArgumentException if the ciphertext for the given plaintext would be larger than the maximum allowed
* Noise transport message size
* @throws ShortBufferException if the given ciphertext buffer does not have enough remaining capacity to hold the
* resulting ciphertext and AEAD tag
*
* @see #getCiphertextLength(int)
*/
int writeMessage(final ByteBuffer plaintext, final ByteBuffer ciphertext) throws ShortBufferException;
/**
* Encrypts a Noise transport message, returning a byte array sized exactly to contain the resulting ciphertext.
*
* @param plaintext the plaintext to encrypt
*
* @return a new byte array containing the resulting ciphertext
*
* @throws IllegalArgumentException if the ciphertext for the given plaintext would be larger than the maximum allowed
* Noise transport message size
*/
byte[] writeMessage(final byte[] plaintext);
/**
* Encrypts a Noise transport message. Callers are responsible for ensuring that the given ciphertext array is large
* enough to hold the resulting ciphertext and AEAD tag.
* <p>
* Note that the ciphertext and plaintext arrays may refer to the same array, allowing for in-place encryption.
*
* @param plaintext a byte array containing the plaintext to encrypt
* @param plaintextOffset the offset within {@code plaintext} where the plaintext begins
* @param plaintextLength the length of the plaintext within {@code plaintext}
* @param ciphertext a byte array into which to write the ciphertext and AEAD tag from this encryption operation
* @param ciphertextOffset the position within {@code ciphertext} at which to begin writing the ciphertext and AEAD
* tag
*
* @return the number of bytes written into the ciphertext array
*
* @throws ShortBufferException if the ciphertext array (after its offset) is too small to hold the resulting
* ciphertext and AEAD tag
* @throws IndexOutOfBoundsException if the given plaintext length exceeds the length of the plaintext array after its
* offset
* @throws IllegalArgumentException if the ciphertext for the given plaintext would be larger than the maximum allowed
* Noise transport message size
*
* @see #getCiphertextLength(int)
*/
int writeMessage(final byte[] plaintext,
final int plaintextOffset,
final int plaintextLength,
final byte[] ciphertext,
final int ciphertextOffset) throws ShortBufferException;
/**
* Sets the encryption key used by this writer to a new pseudo-random key derived from the current key. This operation
* must be coordinated with the receiving party, otherwise messages sent to the receiving party will be unintelligible
* and decrypting future messages will fail.
*/
void rekeyWriter();
}

View File

@ -0,0 +1,141 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.annotation.Nullable;
import javax.crypto.*;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;
abstract class AbstractNoiseCipher implements NoiseCipher {
private final Cipher cipher;
AbstractNoiseCipher(final Cipher cipher) {
this.cipher = cipher;
}
@FunctionalInterface
private interface CipherFinalizer<T> {
T doFinal() throws IllegalBlockSizeException, BadPaddingException, ShortBufferException;
}
protected abstract AlgorithmParameterSpec getAlgorithmParameters(final long nonce);
@Override
public int encrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final ByteBuffer plaintext,
final ByteBuffer ciphertext) throws ShortBufferException {
initCipher(cipher, Cipher.ENCRYPT_MODE, key, nonce);
if (associatedData != null) {
cipher.updateAAD(associatedData);
}
return finishEncryption(() -> cipher.doFinal(plaintext, ciphertext));
}
@Override
public int encrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final byte[] plaintext,
final int plaintextOffset,
final int plaintextLength,
final byte[] ciphertext,
final int ciphertextOffset) throws ShortBufferException {
initCipher(cipher, Cipher.ENCRYPT_MODE, key, nonce);
if (associatedData != null) {
cipher.updateAAD(associatedData);
}
return finishEncryption(() ->
cipher.doFinal(plaintext, plaintextOffset, plaintextLength, ciphertext, ciphertextOffset));
}
@Override
public int decrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final ByteBuffer ciphertext,
final ByteBuffer plaintext) throws AEADBadTagException, ShortBufferException {
initCipher(cipher, Cipher.DECRYPT_MODE, key, nonce);
if (associatedData != null) {
cipher.updateAAD(associatedData);
}
return finishDecryption(() -> cipher.doFinal(ciphertext, plaintext));
}
@Override
public int decrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final byte[] ciphertext,
final int ciphertextOffset,
final int ciphertextLength,
final byte[] plaintext,
final int plaintextOffset) throws AEADBadTagException, ShortBufferException {
initCipher(cipher, Cipher.DECRYPT_MODE, key, nonce);
if (associatedData != null) {
cipher.updateAAD(associatedData);
}
return finishDecryption(() ->
cipher.doFinal(ciphertext, ciphertextOffset, ciphertextLength, plaintext, plaintextOffset));
}
protected void initCipher(final Cipher cipher, final int mode, final Key key, final long nonce) {
final AlgorithmParameterSpec algorithmParameterSpec = getAlgorithmParameters(nonce);
try {
cipher.init(mode, key, algorithmParameterSpec);
} catch (final InvalidAlgorithmParameterException e) {
// This should never happen for a known algorithm with a known "shape" of parameters
throw new AssertionError(e);
} catch (final InvalidKeyException e) {
// This should never happen for a key we control
throw new AssertionError(e);
}
}
private static <T> T finishDecryption(final CipherFinalizer<T> finalizer)
throws AEADBadTagException, ShortBufferException {
try {
return finalizer.doFinal();
} catch (final IllegalBlockSizeException e) {
// We're not using a block cipher
throw new AssertionError(e);
} catch (final BadPaddingException e) {
if (e instanceof AEADBadTagException aeadBadTagException) {
throw aeadBadTagException;
}
// We're also not using padding
throw new AssertionError(e);
}
}
private static <T> T finishEncryption(final CipherFinalizer<T> finalizer) throws ShortBufferException {
try {
return finalizer.doFinal();
} catch (final IllegalBlockSizeException e) {
// We're not using a block cipher
throw new AssertionError(e);
} catch (final BadPaddingException e) {
// We're also not using padding
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,98 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.crypto.KeyAgreement;
import java.security.*;
import java.security.interfaces.XECKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.NamedParameterSpec;
import java.security.spec.X509EncodedKeySpec;
abstract class AbstractXECKeyAgreement implements NoiseKeyAgreement {
private final KeyAgreement keyAgreement;
private final KeyPairGenerator keyPairGenerator;
private final KeyFactory keyFactory;
protected AbstractXECKeyAgreement(final KeyAgreement keyAgreement,
final KeyPairGenerator keyPairGenerator,
final KeyFactory keyFactory) {
this.keyAgreement = keyAgreement;
this.keyPairGenerator = keyPairGenerator;
this.keyFactory = keyFactory;
}
protected abstract byte[] getX509Prefix();
@Override
public KeyPair generateKeyPair() {
return keyPairGenerator.generateKeyPair();
}
@Override
public byte[] generateSecret(final PrivateKey privateKey, final PublicKey publicKey) {
try {
keyAgreement.init(privateKey);
keyAgreement.doPhase(publicKey, true);
return keyAgreement.generateSecret();
} catch (final InvalidKeyException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public byte[] serializePublicKey(final PublicKey publicKey) {
// This is a little hacky, but the structure for an X.509 public key defines the order in which its elements appear.
// The first part of the key, which defines the algorithm and its parameters, is always the same for keys of the
// same type, and the last N bytes are the literal key material.
final byte[] serializedPublicKey = new byte[getPublicKeyLength()];
System.arraycopy(publicKey.getEncoded(), getX509Prefix().length, serializedPublicKey, 0, getPublicKeyLength());
return serializedPublicKey;
}
@Override
public PublicKey deserializePublicKey(final byte[] publicKeyBytes) {
final int publicKeyLength = getPublicKeyLength();
if (publicKeyBytes.length != publicKeyLength) {
throw new IllegalArgumentException("Unexpected serialized public key length");
}
final byte[] x509Prefix = getX509Prefix();
final byte[] x509Bytes = new byte[publicKeyLength + x509Prefix.length];
System.arraycopy(x509Prefix, 0, x509Bytes, 0, x509Prefix.length);
System.arraycopy(publicKeyBytes, 0, x509Bytes, x509Prefix.length, publicKeyLength);
try {
return keyFactory.generatePublic(new X509EncodedKeySpec(x509Bytes, keyFactory.getAlgorithm()));
} catch (final InvalidKeySpecException e) {
throw new IllegalArgumentException("Invalid key", e);
}
}
@Override
public void checkPublicKey(final PublicKey publicKey) throws InvalidKeyException {
checkKey(publicKey);
}
@Override
public void checkKeyPair(final KeyPair keyPair) throws InvalidKeyException {
checkKey(keyPair.getPublic());
checkKey(keyPair.getPrivate());
}
private void checkKey(final Key key) throws InvalidKeyException {
if (key instanceof XECKey xecKey) {
if (xecKey.getParams() instanceof NamedParameterSpec namedParameterSpec) {
if (!keyAgreement.getAlgorithm().equals(namedParameterSpec.getName())) {
throw new InvalidKeyException("Unexpected key algorithm: " + namedParameterSpec.getName());
}
} else {
throw new InvalidKeyException("Unexpected key parameter type: " + xecKey.getParams().getClass());
}
} else {
throw new InvalidKeyException("Unexpected key type: " + key.getClass());
}
}
}

View File

@ -0,0 +1,40 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
class AesGcmCipher extends AbstractNoiseCipher {
AesGcmCipher() {
super(getCipher());
}
private static Cipher getCipher() {
try {
return Cipher.getInstance("AES/GCM/NoPadding");
} catch (final NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new AssertionError("All Java implementations must support AES/GCM/NoPadding");
}
}
@Override
protected AlgorithmParameterSpec getAlgorithmParameters(final long nonce) {
return new GCMParameterSpec(128, ByteBuffer.allocate(12).putLong(4, nonce).array());
}
@Override
public String getName() {
return "AESGCM";
}
@Override
public Key buildKey(final byte[] keyBytes) {
return new SecretKeySpec(keyBytes, "AES");
}
}

View File

@ -0,0 +1,38 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import com.sparrowwallet.lark.bitbox02.noise.crypto.Blake2b512MessageDigest;
import com.sparrowwallet.lark.bitbox02.noise.crypto.HmacBlake2b512Mac;
import javax.crypto.Mac;
import java.security.MessageDigest;
class Blake2bNoiseHash implements NoiseHash {
private final Blake2b512MessageDigest messageDigest;
private final HmacBlake2b512Mac hmac;
public Blake2bNoiseHash() {
this.messageDigest = new Blake2b512MessageDigest();
this.hmac = new HmacBlake2b512Mac();
}
@Override
public String getName() {
return "BLAKE2b";
}
@Override
public MessageDigest getMessageDigest() {
return messageDigest;
}
@Override
public Mac getHmac() {
return hmac;
}
@Override
public int getHashLength() {
return 64;
}
}

View File

@ -0,0 +1,38 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import com.sparrowwallet.lark.bitbox02.noise.crypto.Blake2s256MessageDigest;
import com.sparrowwallet.lark.bitbox02.noise.crypto.HmacBlake2s256Mac;
import javax.crypto.Mac;
import java.security.MessageDigest;
class Blake2sNoiseHash implements NoiseHash {
private final Blake2s256MessageDigest messageDigest;
private final HmacBlake2s256Mac hmac;
public Blake2sNoiseHash() {
this.messageDigest = new Blake2s256MessageDigest();
this.hmac = new HmacBlake2s256Mac();
}
@Override
public String getName() {
return "BLAKE2s";
}
@Override
public MessageDigest getMessageDigest() {
return messageDigest;
}
@Override
public Mac getHmac() {
return hmac;
}
@Override
public int getHashLength() {
return 32;
}
}

View File

@ -0,0 +1,49 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
class ChaCha20Poly1305Cipher extends AbstractNoiseCipher {
private static final String ALGORITHM = "ChaCha20-Poly1305";
public ChaCha20Poly1305Cipher() throws NoSuchAlgorithmException {
super(getCipher());
}
private static Cipher getCipher() {
try {
return Cipher.getInstance(ALGORITHM);
} catch (final NoSuchPaddingException e) {
// This should never happen since we're not specifying a padding
throw new AssertionError("Padding not supported, but no padding specified", e);
} catch (final NoSuchAlgorithmException e) {
// This should never happen since we were able to get an instance of this cipher at construction time
throw new RuntimeException(e);
}
}
@Override
protected AlgorithmParameterSpec getAlgorithmParameters(final long nonce) {
return new IvParameterSpec(ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN)
.putLong(4, nonce)
.array());
}
@Override
public String getName() {
return "ChaChaPoly";
}
@Override
public Key buildKey(final byte[] keyBytes) {
return new SecretKeySpec(keyBytes, "RAW");
}
}

View File

@ -0,0 +1,395 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.annotation.Nullable;
import javax.crypto.AEADBadTagException;
import javax.crypto.ShortBufferException;
import java.nio.ByteBuffer;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
/**
* A Noise cipher is a stateless object that encrypts and decrypts data for use in a Noise protocol. Noise cipher
* implementations must operate in AEAD mode, produce a 16-byte AEAD tag when encrypting data, and verify a 16-byte
* AEAD tag when decrypting data.
*/
public interface NoiseCipher {
/**
* <p>Returns a {@code NoiseCipher} instance that implements the named cipher algorithm. This method recognizes the
* following cipher names:</p>
*
* <dl>
* <dt>ChaChaPoly</dt>
* <dd>Returns a Noise cipher implementation backed by the {@link javax.crypto.Cipher} returned by the most
* preferred security provider that supports the "ChaCha20-Poly1305" cipher transformation</dd>
*
* <dt>AESGCM</dt>
* <dd>Returns a Noise cipher implementation backed by the {@link javax.crypto.Cipher} returned by the most
* preferred security provider that supports the "AES/GCM/NoPadding" cipher transformation</dd>
* </dl>
*
* <p>Every implementation of the Java platform is required to support the "AES/GCM/NoPadding" cipher transformation,
* which underpins the "AESGCM" Noise cipher.</p>
*
* @param noiseCipherName the name of the Noise cipher algorithm for which to return a concrete {@code NoiseCipher}
* implementation
*
* @return a concrete {@code NoiseCipher} implementation for the given algorithm name
*
* @throws NoSuchAlgorithmException if the given name is "ChaChaPoly" and the "ChaCha20-Poly1305" cipher
* transformation is not supported by any security provider in the current JVM
* @throws IllegalArgumentException if the given name is not a known Noise cipher name
*
* @see javax.crypto.Cipher#getInstance(String)
*/
static NoiseCipher getInstance(final String noiseCipherName) throws NoSuchAlgorithmException {
return switch (noiseCipherName) {
case "ChaChaPoly" -> new ChaCha20Poly1305Cipher();
case "AESGCM" -> new AesGcmCipher();
default -> throw new IllegalArgumentException("Unrecognized Noise cipher name: " + noiseCipherName);
};
}
/**
* Returns the name of this Noise cipher as it would appear in a full Noise protocol name.
*
* @return the name of this Noise cipher as it would appear in a full Noise protocol name
*/
String getName();
/**
* <p>Encrypts the given plaintext using the given key, nonce, and associated data. This method returns a new byte
* buffer sized exactly to contain the resulting ciphertext and AEAD tag.</p>
*
* <p>All {@code plaintext.remaining()} bytes starting at {@code plaintext.position()} are processed. Upon return, the
* plaintext buffer's position will be equal to its limit; its limit will not have changed. If associated data is
* provided, the same is true of the associated data buffer. The returned ciphertext buffer's position will be zero,
* and its limit will be equal to its capacity.</p>
*
* @param key the key with which to encrypt the given plaintext
* @param nonce a nonce, which must be unique for the given key
* @param associatedData the associated data to use when calculating an AEAD tag
* @param plaintext the plaintext to encrypt
*
* @return a new byte buffer containing the resulting ciphertext and AEAD tag
*
* @see #getCiphertextLength(int)
*/
default ByteBuffer encrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final ByteBuffer plaintext) {
final ByteBuffer ciphertext = ByteBuffer.allocate(getCiphertextLength(plaintext.remaining()));
try {
encrypt(key, nonce, associatedData, plaintext, ciphertext);
ciphertext.flip();
} catch (final ShortBufferException e) {
// This should never happen for a buffer we control
throw new AssertionError(e);
}
return ciphertext;
}
/**
* <p>Encrypts the given plaintext using the given key, nonce, and associated data. Callers are responsible for
* ensuring that the given ciphertext buffer has enough remaining capacity to hold the resulting ciphertext and AEAD
* tag.</p>
*
* <p>All {@code plaintext.remaining()} bytes starting at {@code plaintext.position()} are processed. Upon return, the
* plaintext buffer's position will be equal to its limit; its limit will not have changed. If associated data is
* provided, the same will be true of the associated data buffer. The ciphertext buffer's position will have advanced
* by n, where n is the value returned by this method; the ciphertext buffer's limit will not have changed.</p>
*
* <p>Note that the ciphertext and plaintext buffers must be different, but may refer to the same underlying byte
* array to facilitate in-place encryption.</p>
*
* @param key the key with which to encrypt the given plaintext
* @param nonce a nonce, which must be unique for the given key
* @param associatedData the associated data to use when calculating an AEAD tag
* @param plaintext the plaintext to encrypt
* @param ciphertext the buffer into which to write the resulting ciphertext and AEAD tag
*
* @return the number of bytes written into the ciphertext buffer
*
* @throws ShortBufferException if the given ciphertext buffer does not have enough remaining capacity to hold the
* resulting ciphertext and AEAD tag
*
* @see #getCiphertextLength(int)
*/
int encrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final ByteBuffer plaintext,
final ByteBuffer ciphertext)
throws ShortBufferException;
/**
* Encrypts the given plaintext using the given key, nonce, and associated data. This method returns a new byte array
* sized exactly to contain the resulting ciphertext and AEAD tag.
*
* @param key the key with which to encrypt the given plaintext
* @param nonce a nonce, which must be unique for the given key
* @param associatedData the associated data to use when calculating an AEAD tag
* @param plaintext the plaintext to encrypt
*
* @return a new byte array containing the resulting ciphertext and AEAD tag
*
* @see #getCiphertextLength(int)
*/
default byte[] encrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final byte[] plaintext) {
final byte[] ciphertext = new byte[getCiphertextLength(plaintext.length)];
try {
encrypt(key,
nonce,
associatedData,
plaintext,
0,
plaintext.length,
ciphertext,
0);
} catch (final ShortBufferException e) {
// This should never happen for a buffer we control
throw new AssertionError(e);
}
return ciphertext;
}
/**
* <p>Encrypts the given plaintext using the given key, nonce, and associated data. Callers are responsible for
* ensuring that the given ciphertext array is large enough to hold the resulting ciphertext and AEAD tag.</p>
*
* <p>Note that the ciphertext and plaintext arrays may refer to the same array, allowing for in-place encryption.</p>
*
* @param key the key with which to encrypt the given plaintext
* @param nonce a nonce, which must be unique for the given key
* @param associatedData a byte array containing the associated data (if any) to be used when encrypting the given
* plaintext; may be {@code null}
* @param plaintext a byte array containing the plaintext to encrypt
* @param plaintextOffset the offset within {@code plaintext} where the plaintext begins
* @param plaintextLength the length of the plaintext within {@code plaintext}
* @param ciphertext a byte array into which to write the ciphertext and AEAD tag from this encryption operation
* @param ciphertextOffset the position within {@code ciphertext} at which to begin writing the ciphertext and AEAD
* tag
*
* @return the number of bytes written into the ciphertext array
*
* @throws ShortBufferException if the ciphertext array (after its offset) is too small to hold the resulting
* ciphertext and AEAD tag
* @throws IndexOutOfBoundsException if the given plaintext length exceeds the length of the plaintext array after its
* offset
*
* @see #getCiphertextLength(int)
*/
int encrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final byte[] plaintext,
final int plaintextOffset,
final int plaintextLength,
final byte[] ciphertext,
final int ciphertextOffset) throws ShortBufferException;
/**
* <p>Decrypts the given ciphertext and verifies its AEAD tag using the given key, nonce, and associated data. This
* method returns a new {@link ByteBuffer} sized exactly to contain the resulting plaintext. The returned buffer's
* position will be zero, and its limit and capacity will be equal to the plaintext length.</p>
*
* <p>All {@code ciphertext.remaining()} bytes starting at {@code ciphertext.position()} are processed. Upon return,
* the ciphertext buffer's position will be equal to its limit; its limit will not have changed. If associated data is
* provided, the same will be true of the associated data buffer.</p>
*
* @param key the key with which to decrypt the given ciphertext
* @param nonce a nonce, which must be unique for the given key
* @param associatedData the associated data to use when verifying the AEAD tag; may be {@code null}
* @param ciphertext the ciphertext to decrypt
*
* @return a {@code ByteBuffer} containing the resulting plaintext
*
* @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value
* @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag
*
* @see #getPlaintextLength(int)
*/
default ByteBuffer decrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final ByteBuffer ciphertext) throws AEADBadTagException {
final ByteBuffer plaintext = ByteBuffer.allocate(getPlaintextLength(ciphertext.remaining()));
try {
decrypt(key, nonce, associatedData, ciphertext, plaintext);
plaintext.rewind();
} catch (final ShortBufferException e) {
// This should never happen for a buffer we control
throw new AssertionError(e);
}
return plaintext;
}
/**
* <p>Decrypts the given ciphertext and verifies its AEAD tag using the given key, nonce, and associated data. This
* method writes the resulting plaintext into the given {@code plaintext} buffer. Callers are responsible for ensuring
* that the given plaintext buffer has enough remaining capacity to hold the resulting plaintext.</p>
*
* <p>All {@code ciphertext.remaining()} bytes starting at {@code ciphertext.position()} are processed. Upon return,
* the ciphertext buffer's position will be equal to its limit; its limit will not have changed. If associated data is
* provided, the same will be true of the associated data buffer. The plaintext buffer's position will have advanced
* by n, where n is the value returned by this method; the plaintext buffer's limit will not have changed.</p>
*
* @param key the key with which to decrypt the given ciphertext
* @param nonce a nonce, which must be unique for the given key
* @param associatedData the associated data to use when verifying the AEAD tag; may be {@code null}
* @param ciphertext the ciphertext to decrypt
* @param plaintext the buffer into which to write the resulting plaintext
*
* @return the number of bytes written into the {@code plaintext} buffer
*
* @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value
* @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag
* @throws ShortBufferException if the given plaintext buffer does not have enough remaining capacity to hold the
* resulting plaintext
*
* @see #getPlaintextLength(int)
*/
int decrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final ByteBuffer ciphertext,
final ByteBuffer plaintext)
throws AEADBadTagException, ShortBufferException;
/**
* Decrypts the given ciphertext and verifies its AEAD tag using the given key, nonce, and associated data. This
* method returns a new byte array sized exactly to contain the resulting plaintext.
*
* @param key the key with which to decrypt the given ciphertext
* @param nonce a nonce, which must be unique for the given key
* @param associatedData the associated data to use when verifying the AEAD tag; may be {@code null}
* @param ciphertext the ciphertext to decrypt
*
* @return a byte array containing the resulting plaintext
*
* @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value
* @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag
*
* @see #getPlaintextLength(int)
*/
default byte[] decrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final byte[] ciphertext) throws AEADBadTagException {
final byte[] plaintext = new byte[getPlaintextLength(ciphertext.length)];
try {
decrypt(key,
nonce,
associatedData,
ciphertext,
0,
ciphertext.length,
plaintext,
0);
} catch (final ShortBufferException e) {
// This should never happen for a buffer we control
throw new AssertionError(e);
}
return plaintext;
}
/**
* <p>Decrypts the given ciphertext and verifies its AEAD tag. This writes the resulting plaintext into a provided
* byte array.</p>
*
* <p>Note that {@code ciphertext} and {@code plaintext} may refer to the same byte array, allowing for in-place
* decryption.</p>
*
* @param key the key with which to decrypt the given plaintext
* @param nonce a nonce, which must be unique for the given key
* @param associatedData a byte array containing the associated data (if any) to be used when verifying the AEAD tag
* for the given ciphertext; may be {@code null}
* @param ciphertext a byte array containing the ciphertext and AEAD tag to be decrypted and verified
* @param ciphertextOffset the position within {@code ciphertext} at which to begin reading the ciphertext and AEAD
* tag
* @param ciphertextLength the length of the ciphertext and AEAD tag within {@code ciphertext}
* @param plaintext a byte array into which to write the decrypted plaintext
* @param plaintextOffset the offset within {@code plaintext} where the plaintext begins
*
* @return the number of bytes written to {@code plaintext}
*
* @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value
* @throws ShortBufferException if {@code plaintext} is not long enough (after its offset) to contain the resulting
* plaintext
* @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag
*
* @see #getPlaintextLength(int)
*/
int decrypt(final Key key,
final long nonce,
@Nullable final byte[] associatedData,
final byte[] ciphertext,
final int ciphertextOffset,
final int ciphertextLength,
final byte[] plaintext,
final int plaintextOffset) throws AEADBadTagException, ShortBufferException;
/**
* Returns the size of a buffer needed to hold the ciphertext produced by encrypting a plaintext of the given length
* (the length of the plaintext plus the length of an AEAD tag).
*
* @param plaintextLength the length of a plaintext
*
* @return the length of the ciphertext that would be produced by encrypting a plaintext of the given length
*/
default int getCiphertextLength(final int plaintextLength) {
return plaintextLength + 16;
}
/**
* Returns the size of a buffer needed to hold the plaintext produced by decrypting a ciphertext of the given length
* (the length of the ciphertext minus the length of the AEAD tag).
*
* @param ciphertextLength the length of a ciphertext
*
* @return the length of the plaintext that would be produced by decrypting a ciphertext of the given length
*/
default int getPlaintextLength(final int ciphertextLength) {
if (ciphertextLength < 16) {
throw new IllegalArgumentException("Ciphertexts must be at least 16 bytes long");
}
return ciphertextLength - 16;
}
/**
* Converts an array of bytes into a {@link Key} instance suitable for use with this cipher.
*
* @param keyBytes the raw bytes of the key
*
* @return a {@code Key} suitable for use with this cipher
*/
Key buildKey(byte[] keyBytes);
/**
* Generates a new pseudo-random key as a function of the given key.
*
* @param key the key from which to derive a new key
*
* @return a new pseudo-random key derived from the given key
*/
default Key rekey(final Key key) {
return buildKey(encrypt(key, 0xffffffffffffffffL, null, new byte[32]));
}
}

View File

@ -0,0 +1,141 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* A Noise hash implementation encapsulates the hashing functionality of a Noise protocol. A Noise hash provides
* {@link MessageDigest} instances that implement the Noise hash's hashing algorithm, {@link Mac} instances using the
* same algorithm for calculating HMAC digests, and key derivation function.
*/
public interface NoiseHash {
/**
* <p>Returns a {@code NoiseHash} instance that implements the named hash algorithm. This method recognizes the
* following hash names:</p>
*
* <dl>
* <dt>SHA256</dt>
* <dd>Returns a Noise hash implementation backed by the {@link MessageDigest} returned by the most
* preferred security provider that supports the "SHA-256" algorithm and the {@link Mac} returned by
* the most preferred security provider that supports the "HmacSHA256" algorithm</dd>
*
* <dt>SHA512</dt>
* <dd>Returns a Noise hash implementation backed by the {@link MessageDigest} returned by the most
* preferred security provider that supports the "SHA-512" algorithm and the {@link Mac} returned by
* the most preferred security provider that supports the "HmacSHA512" algorithm</dd>
*
* <dt>BLAKE2s</dt>
* <dd>Returns a Noise hash implementation backed by BLAKE2s implementations included in java-noise</dd>
*
* <dt>BLAKE2b</dt>
* <dd>Returns a Noise hash implementation backed by BLAKE2b implementations included in java-noise</dd>
* </dl>
*
* <p>Every implementation of the Java platform is required to support the "SHA-256" and "HmacSHA256" algorithms.
* Java-noise provides its own BLAKE2b/BLAKE2s implementations.</p>
*
* @param noiseHashName the name of the Noise hash algorithm for which to return a concrete {@code NoiseHash}
* implementation
*
* @return a concrete {@code NoiseCipher} implementation for the given algorithm name
*
* @throws NoSuchAlgorithmException if the given name is "SHA512" and either the "SHA-512" or "HmacSHA512" algorithm
* is not supported by any security provider in the current JVM
* @throws IllegalArgumentException if the given name is not a known Noise hash name
*
* @see MessageDigest#getInstance(String)
* @see Mac#getInstance(String)
*/
static NoiseHash getInstance(final String noiseHashName) throws NoSuchAlgorithmException {
return switch (noiseHashName) {
case "SHA256" -> new Sha256NoiseHash();
case "SHA512" -> new Sha512NoiseHash();
case "BLAKE2s" -> new Blake2sNoiseHash();
case "BLAKE2b" -> new Blake2bNoiseHash();
default -> throw new IllegalArgumentException("Unrecognized hash name: " + noiseHashName);
};
}
/**
* Returns the name of this Noise hash as it would appear in a full Noise protocol name.
*
* @return the name of this Noise hash as it would appear in a full Noise protocol name
*/
String getName();
/**
* Returns a new {@link MessageDigest} for calculating hashes using this Noise hash's hashing algorithm.
*
* @return a new {@link MessageDigest} for calculating hashes
*/
MessageDigest getMessageDigest();
/**
* Returns a new {@link Mac} instance for calculating HMAC digests using this Noise hash's hashing algorithm.
*
* @return a new {@code Mac} instance for calculating HMAC digests
*/
Mac getHmac();
/**
* Returns the length of a digest produced by the {@link MessageDigest} or {@link Mac} provided by this Noise hash.
*
* @return the length of a digest produced by this Noise hash
*/
int getHashLength();
/**
* <p>Derives two or three pseudo-random keys from the given chaining key and input key material using the HKDF
* algorithm with this Noise hash's HMAC algorithm.</p>
*
* <p>As the Noise Protocol Framework specification notes:</p>
*
* <blockquote>Note that [the derived keys] are all [{@link #getHashLength()}] bytes in length. Also note that the
* [{@code deriveKeys}] function is simply HKDF from [IETF RFC 5869] with the chaining_key as HKDF salt, and
* zero-length HKDF info.</blockquote>
*
* @param chainingKey the chaining key (salt) from which to derive new keys
* @param inputKeyMaterial the input key material from which to derive new keys
* @param outputKeys the number of keys to derive; must be either 2 or 3
*
* @return an array containing {@code outputKeys} derived keys
*
* @see <a href="https://www.ietf.org/rfc/rfc5869.txt">IETF RFC 5869: HMAC-based Extract-and-Expand Key Derivation
* Function (HKDF)</a>
*/
default byte[][] deriveKeys(final byte[] chainingKey, final byte[] inputKeyMaterial, final int outputKeys) {
if (outputKeys < 2 || outputKeys > 3) {
throw new IllegalArgumentException("Illegal output key count");
}
final byte[][] derivedKeys = new byte[getHashLength()][outputKeys];
final Mac hmac = getHmac();
try {
hmac.init(new SecretKeySpec(chainingKey, "RAW"));
final Key tempKey = new SecretKeySpec(hmac.doFinal(inputKeyMaterial), "RAW");
for (byte k = 0; k < outputKeys; k++) {
hmac.init(tempKey);
if (k > 0) {
hmac.update(derivedKeys[k - 1]);
}
hmac.update((byte) (k + 1));
derivedKeys[k] = hmac.doFinal();
}
return derivedKeys;
} catch (final InvalidKeyException e) {
// This should never happen for keys we derive/control
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,122 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.crypto.KeyAgreement;
import java.security.*;
/**
* A Noise key agreement implementation encapsulates the key agreement functions of a Noise protocol. A Noise key
* agreement generates key pairs for key agreement operations with the remote party in a Noise handshake, performs key
* agreement operations, and converts keys to and from "raw" formats for serialization in Noise messages.
*/
public interface NoiseKeyAgreement {
/**
* Returns a {@code NoiseKeyAgreement} instance that implements the named key agreement algorithm. This method
* recognizes the following key agreement algorithm names:
* <dl>
* <dt>25519</dt>
* <dd>Returns a Noise key agreement implementation backed by the {@link KeyAgreement} returned by the
* most preferred security provider that supports the "X25519" algorithm</dd>
*
* <dt>448</dt>
* <dd>Returns a Noise key agreement implementation backed by the {@link KeyAgreement} returned by the
* most preferred security provider that supports the "X448" algorithm</dd>
* </dl>
*
* @param noiseKeyAgreementName the name of the Noise key agreement algorithm for which to return a concrete
* {@code NoiseKeyAgreement} implementation
*
* @return a concrete {@code NoiseKeyAgreement} implementation for the given algorithm name
*
* @throws NoSuchAlgorithmException if the given name is a known Noise key agreement name, but the underlying key
* agreement algorithm is not supported by any security provider in the current JVM
* @throws IllegalArgumentException if the given name is not a known Noise key agreement name
*
* @see KeyAgreement#getInstance(String)
*/
static NoiseKeyAgreement getInstance(final String noiseKeyAgreementName) throws NoSuchAlgorithmException {
return switch (noiseKeyAgreementName) {
case "25519" -> new X25519KeyAgreement();
case "448" -> new X448KeyAgreement();
default -> throw new IllegalArgumentException("Unrecognized key agreement name: " + noiseKeyAgreementName);
};
}
/**
* Returns the name of this Noise key agreement as it would appear in a full Noise protocol name.
*
* @return the name of this Noise key agreement as it would appear in a full Noise protocol name
*/
String getName();
/**
* Generates a new key pair compatible with this key agreement algorithm for use in a Noise handshake.
*
* @return a new key pair for use in a Noise handshake
*/
KeyPair generateKeyPair();
/**
* Calculates a shared secret from a local private key and a remote public key.
*
* @param privateKey the local private key from which to calculate a shared secret
* @param publicKey the remote public key from which to calculate a shared secret
*
* @return a shared secret of length {@link #getPublicKeyLength()}
*
* @throws IllegalArgumentException if either the local private key or remote public key is not a valid key for this
* key agreement algorithm
*/
byte[] generateSecret(PrivateKey privateKey, PublicKey publicKey);
/**
* Returns the length of public keys and shared secrets generated by this key agreement algorithm.
*
* @return the length of public keys and shared secrets generated by this key agreement algorithm
*/
int getPublicKeyLength();
/**
* Serializes a public key compatible with this key agreement algorithm to an array of bytes suitable for transmission
* in a Noise handshake message.
*
* @param publicKey the public key to serialize as an array of bytes
*
* @return a byte array containing the "raw" public key
*
* @see #deserializePublicKey(byte[])
*/
byte[] serializePublicKey(PublicKey publicKey);
/**
* Interprets a "raw" public key as a {@link PublicKey} compatible with this key agreement algorithm.
*
* @param publicKeyBytes the "raw" public key bytes to interpret; must have a length of {@link #getPublicKeyLength()}
*
* @return a {@code PublicKey} instance defined by the given {@code publicKeyBytes}
*
* @throws IllegalArgumentException if the given array of bytes could not be interpreted as a public key compatible
* with this key agreement algorithm for any reason
*
* @see #serializePublicKey(PublicKey)
*/
PublicKey deserializePublicKey(byte[] publicKeyBytes);
/**
* Checks that the given public key is compatible with this key agreement algorithm.
*
* @param publicKey the public key to check for compatibility with this key agreement algorithm
*
* @throws InvalidKeyException if the given key is not compatible with this key agreement algorithm
*/
void checkPublicKey(PublicKey publicKey) throws InvalidKeyException;
/**
* Checks that both of the keys in the given key pair are compatible with this key agreement algorithm.
*
* @param keyPair the key pair to check for compatibility with this key agreement algorithm
*
* @throws InvalidKeyException if either key in the given key pair is not compatible with this key agreement algorithm
*/
void checkKeyPair(KeyPair keyPair) throws InvalidKeyException;
}

View File

@ -0,0 +1,36 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.crypto.Mac;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
class Sha256NoiseHash implements NoiseHash {
@Override
public String getName() {
return "SHA256";
}
@Override
public MessageDigest getMessageDigest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (final NoSuchAlgorithmException e) {
throw new AssertionError("Every implementation of the Java platform is required to support SHA-256", e);
}
}
@Override
public Mac getHmac() {
try {
return Mac.getInstance("HmacSHA256");
} catch (final NoSuchAlgorithmException e) {
throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256", e);
}
}
@Override
public int getHashLength() {
return 32;
}
}

View File

@ -0,0 +1,45 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.crypto.Mac;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
class Sha512NoiseHash implements NoiseHash {
private static final String MESSAGE_DIGEST_ALGORITHM = "SHA-512";
private static final String HMAC_ALGORITHM = "HmacSHA512";
public Sha512NoiseHash() throws NoSuchAlgorithmException {
// Fail fast: check once if SHA-512 is supported so we don't have to worry about exceptions later
MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM);
Mac.getInstance(HMAC_ALGORITHM);
}
@Override
public String getName() {
return "SHA512";
}
@Override
public MessageDigest getMessageDigest() {
try {
return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM);
} catch (final NoSuchAlgorithmException e) {
throw new AssertionError("Previously-available message digest algorithm must remain available", e);
}
}
@Override
public Mac getHmac() {
try {
return Mac.getInstance(HMAC_ALGORITHM);
} catch (final NoSuchAlgorithmException e) {
throw new AssertionError("Previously-available HMAC algorithm must remain available", e);
}
}
@Override
public int getHashLength() {
return 64;
}
}

View File

@ -0,0 +1,32 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.crypto.KeyAgreement;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
class X25519KeyAgreement extends AbstractXECKeyAgreement {
private static final String ALGORITHM = "X25519";
private static final byte[] X509_PREFIX = HexFormat.of().parseHex("302a300506032b656e032100");
public X25519KeyAgreement() throws NoSuchAlgorithmException {
super(KeyAgreement.getInstance(ALGORITHM), KeyPairGenerator.getInstance(ALGORITHM), KeyFactory.getInstance(ALGORITHM));
}
@Override
public String getName() {
return "25519";
}
@Override
public int getPublicKeyLength() {
return 32;
}
@Override
protected byte[] getX509Prefix() {
return X509_PREFIX;
}
}

View File

@ -0,0 +1,32 @@
package com.sparrowwallet.lark.bitbox02.noise.component;
import javax.crypto.KeyAgreement;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
class X448KeyAgreement extends AbstractXECKeyAgreement {
private static final String ALGORITHM = "X448";
private static final byte[] X509_PREFIX = HexFormat.of().parseHex("3042300506032b656f033900");
public X448KeyAgreement() throws NoSuchAlgorithmException {
super(KeyAgreement.getInstance(ALGORITHM), KeyPairGenerator.getInstance(ALGORITHM), KeyFactory.getInstance(ALGORITHM));
}
@Override
public String getName() {
return "448";
}
@Override
public int getPublicKeyLength() {
return 56;
}
@Override
protected byte[] getX509Prefix() {
return X509_PREFIX;
}
}

Some files were not shown because too many files have changed in this diff Show More