initial commit
This commit is contained in:
commit
7e803996da
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.idea
|
||||
.gradle
|
||||
*iml
|
||||
build
|
||||
/*.properties
|
||||
out
|
||||
*.log
|
||||
.DS_Store
|
||||
177
LICENSE
Normal file
177
LICENSE
Normal 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
23
README.md
Normal 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
36
build.gradle
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
234
gradlew
vendored
Executable 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
89
gradlew.bat
vendored
Normal 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
2
settings.gradle
Normal file
@ -0,0 +1,2 @@
|
||||
rootProject.name = 'lark'
|
||||
|
||||
840
src/main/java/com/sparrowwallet/lark/BitBox02Client.java
Normal file
840
src/main/java/com/sparrowwallet/lark/BitBox02Client.java
Normal 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) {}
|
||||
}
|
||||
17
src/main/java/com/sparrowwallet/lark/Chain.java
Normal file
17
src/main/java/com/sparrowwallet/lark/Chain.java
Normal 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;
|
||||
}
|
||||
}
|
||||
277
src/main/java/com/sparrowwallet/lark/ColdcardClient.java
Normal file
277
src/main/java/com/sparrowwallet/lark/ColdcardClient.java
Normal 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][];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.lark;
|
||||
|
||||
public class DeviceBusyException extends DeviceException {
|
||||
public DeviceBusyException() {
|
||||
super("Device is busy");
|
||||
}
|
||||
}
|
||||
11
src/main/java/com/sparrowwallet/lark/DeviceException.java
Normal file
11
src/main/java/com/sparrowwallet/lark/DeviceException.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.lark;
|
||||
|
||||
public class DeviceFramingException extends DeviceProtocolException {
|
||||
public DeviceFramingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/sparrowwallet/lark/DeviceId.java
Normal file
43
src/main/java/com/sparrowwallet/lark/DeviceId.java
Normal 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() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.lark;
|
||||
|
||||
public class DeviceNotFoundException extends DeviceException {
|
||||
public DeviceNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.lark;
|
||||
|
||||
public class DeviceNotReadyException extends DeviceException {
|
||||
public DeviceNotReadyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.lark;
|
||||
|
||||
public class DeviceProtocolException extends DeviceException {
|
||||
public DeviceProtocolException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
154
src/main/java/com/sparrowwallet/lark/HardwareClient.java
Normal file
154
src/main/java/com/sparrowwallet/lark/HardwareClient.java
Normal 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();
|
||||
}
|
||||
}
|
||||
107
src/main/java/com/sparrowwallet/lark/HardwareType.java
Normal file
107
src/main/java/com/sparrowwallet/lark/HardwareType.java
Normal 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());
|
||||
}
|
||||
}
|
||||
158
src/main/java/com/sparrowwallet/lark/JadeClient.java
Normal file
158
src/main/java/com/sparrowwallet/lark/JadeClient.java
Normal 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();
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/sparrowwallet/lark/KeepkeyClient.java
Normal file
31
src/main/java/com/sparrowwallet/lark/KeepkeyClient.java
Normal 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;
|
||||
}
|
||||
}
|
||||
887
src/main/java/com/sparrowwallet/lark/Lark.java
Normal file
887
src/main/java/com/sparrowwallet/lark/Lark.java
Normal 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) {
|
||||
}
|
||||
}
|
||||
457
src/main/java/com/sparrowwallet/lark/LedgerClient.java
Normal file
457
src/main/java/com/sparrowwallet/lark/LedgerClient.java
Normal 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) {}
|
||||
}
|
||||
48
src/main/java/com/sparrowwallet/lark/Platform.java
Normal file
48
src/main/java/com/sparrowwallet/lark/Platform.java
Normal 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;
|
||||
}
|
||||
}
|
||||
754
src/main/java/com/sparrowwallet/lark/TrezorClient.java
Normal file
754
src/main/java/com/sparrowwallet/lark/TrezorClient.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
59
src/main/java/com/sparrowwallet/lark/Version.java
Normal file
59
src/main/java/com/sparrowwallet/lark/Version.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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) {}
|
||||
}
|
||||
17
src/main/java/com/sparrowwallet/lark/args/AddrType.java
Normal file
17
src/main/java/com/sparrowwallet/lark/args/AddrType.java
Normal 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;
|
||||
}
|
||||
}
|
||||
50
src/main/java/com/sparrowwallet/lark/args/Args.java
Normal file
50
src/main/java/com/sparrowwallet/lark/args/Args.java
Normal 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;
|
||||
}
|
||||
9
src/main/java/com/sparrowwallet/lark/args/Command.java
Normal file
9
src/main/java/com/sparrowwallet/lark/args/Command.java
Normal 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;
|
||||
}
|
||||
@ -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) {}
|
||||
}
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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) {}
|
||||
}
|
||||
48
src/main/java/com/sparrowwallet/lark/args/SignTxCommand.java
Normal file
48
src/main/java/com/sparrowwallet/lark/args/SignTxCommand.java
Normal 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) {}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.lark.bitbox02;
|
||||
|
||||
public interface PhysicalLayer {
|
||||
void write(byte[] bytes);
|
||||
byte[] read(int size, int timeoutMs);
|
||||
void close();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
146
src/main/java/com/sparrowwallet/lark/bitbox02/U2FHid.java
Normal file
146
src/main/java/com/sparrowwallet/lark/bitbox02/U2FHid.java
Normal 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
25042
src/main/java/com/sparrowwallet/lark/bitbox02/generated/Btc.java
Normal file
25042
src/main/java/com/sparrowwallet/lark/bitbox02/generated/Btc.java
Normal file
File diff suppressed because it is too large
Load Diff
3556
src/main/java/com/sparrowwallet/lark/bitbox02/generated/Common.java
Normal file
3556
src/main/java/com/sparrowwallet/lark/bitbox02/generated/Common.java
Normal file
File diff suppressed because it is too large
Load Diff
10442
src/main/java/com/sparrowwallet/lark/bitbox02/generated/Hww.java
Normal file
10442
src/main/java/com/sparrowwallet/lark/bitbox02/generated/Hww.java
Normal file
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
@ -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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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:
|
||||
* -> e
|
||||
* <- 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:
|
||||
* <- s
|
||||
* ...
|
||||
* -> e, es, s, ss
|
||||
* <- 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);
|
||||
}
|
||||
}
|
||||
@ -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
@ -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 {
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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]));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
Loading…
Reference in New Issue
Block a user