Compare commits

...

94 Commits

Author SHA1 Message Date
Nicolas Dorier
f7ad0e57aa
Make install-arch.sh idempotent
Some checks failed
CI / win-x64 (push) Has been cancelled
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2026-05-06 16:07:10 +09:00
Nicolas Dorier
4a393f2bfe
Update Release
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2026-05-06 15:50:18 +09:00
Nicolas Dorier
7ce3af4209
Add maintainer script in bash 2026-05-06 15:46:50 +09:00
Nicolas Dorier
d93abb5f15
bump hwi 2026-05-06 15:43:51 +09:00
Nicolas Dorier
434051d059
bump .net10.0 and libs 2026-05-06 15:30:30 +09:00
Nicolas Dorier
f631f59ae4
Remove udev rules uneeded in new systemd 2026-05-06 15:18:19 +09:00
Nicolas Dorier
a7ba63fc8f Add arch instructions
Some checks failed
CI / osx-x64 (push) Has been cancelled
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2025-12-10 19:44:44 +09:00
nicolas.dorier
946bbf5111
bump
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2025-10-22 10:17:00 +09:00
nicolas.dorier
0ca3d5fd87
Add libicu 74 or 76 deps to deb package (fix #90) 2025-10-22 10:15:12 +09:00
nicolas.dorier
2924eb58da
Fix build
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2025-07-16 14:14:31 +09:00
nicolas.dorier
f39a1ec369
Update mac instructions 2025-07-15 15:48:35 +09:00
nicolas.dorier
5fde8f1a7d
Update Release notes 2025-06-26 17:09:29 +09:00
nicolas.dorier
0c330b19e4
bump version 2025-06-26 16:24:25 +09:00
nicolas.dorier
1e3329884c
Bump HWI to 3.1.0 2025-06-26 16:23:30 +09:00
nicolas.dorier
ecf0fec03b
bump 2025-04-21 15:35:41 +09:00
nicolas.dorier
e9107c589c
bump 2025-04-21 14:52:17 +09:00
nicolas.dorier
fee92fd70c
bump 2025-04-21 14:51:40 +09:00
nicolas.dorier
eaff86e75d
Returns raw data of enumerated devices 2025-04-21 14:51:03 +09:00
nicolas.dorier
e83947d811
Support DisplayAddress on taproot86 2025-04-17 10:12:56 +09:00
nicolas.dorier
dcff6ce715
Bump HWI
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2024-07-04 09:56:13 +09:00
nicolas.dorier
12e442910b
Bump version
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2024-01-23 11:00:26 +09:00
nicolas.dorier
e4202d11de
Update RELEASE 2024-01-23 10:55:41 +09:00
nicolas.dorier
6999b96588
bump libs 2024-01-23 10:54:49 +09:00
nicolas.dorier
4735c541ce
Fix: Impossible to sign big transactions on windows (#77) 2024-01-23 10:53:38 +09:00
nicolas.dorier
5a34f56c46
Make apple sign faster 2023-12-08 16:42:52 +09:00
nicolas.dorier
c8457c35f9
bump
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2023-12-08 16:19:46 +09:00
nicolas.dorier
39b2e4e0a0
Fix warnings 2023-12-08 16:18:34 +09:00
nicolas.dorier
3e25d227b0
bump CI dotnet version 2023-12-07 16:40:12 +09:00
Jumar Macato
90c36153e6 use the official way of getting platform handles in v11 2023-12-06 20:58:37 +08:00
Jumar Macato
77ae2be45c add simple theme 2023-12-06 20:58:02 +08:00
Jumar Macato
db9c8ec9a5 remove old 0.10.x hacks 2023-12-06 20:57:49 +08:00
Jumar Macato
cf5fa5467e update OnClosing parameters 2023-12-06 20:57:32 +08:00
Jumar Macato
5df7ca949e Update to DrawingImage. 2023-12-06 20:57:19 +08:00
Jumar Macato
d7a5957949 reuse the existing service provider rather than using Avalonia's internal locator (it's deprecated for public use) 2023-12-06 20:56:52 +08:00
nicolas.dorier
3c289338f0
bump avalonia 2023-12-06 20:37:55 +09:00
nicolas.dorier
607d6c6c6e
Bump libraries, NET8.0 2023-12-06 10:05:40 +09:00
nicolas.dorier
ee870089a9
Fix entitlement for macOS
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2023-12-06 09:57:00 +09:00
nicolas.dorier
bc5f7b2cb9
Do not hardcode team-id 2023-12-05 23:59:47 +09:00
nicolas.dorier
17bc7f2e61
Hardcode team-id
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2023-12-05 23:49:36 +09:00
nicolas.dorier
4d02f0f416
Add APPLE_TEAM_ID 2023-12-05 22:37:27 +09:00
nicolas.dorier
9839b6e3c0
Fix notarization process on mac 2023-12-05 22:30:02 +09:00
nicolas.dorier
299314129d
bump 2023-12-05 21:08:33 +09:00
Nicolas Dorier
fdb34c3345
Merge pull request #69 from btcpayserver/nfcsupport
Support NFC smart card readers
2023-12-01 12:46:17 +09:00
nicolas.dorier
cadccd45e9
Support NFC smart card readers 2023-10-30 14:29:59 +09:00
nicolas.dorier
3a660f6b8d
Bump HWI 2023-10-30 10:35:45 +09:00
Pavlenex
e3c431a5af
Merge pull request #57 from Bangalisch/BTCPayVaultUpdate
Update video on BTCPay Vault
2023-03-09 13:32:25 +01:00
Bangalisch
a12777b305 Update video on BTCPay Vault
Yo

webworthy made new video, also changed docs to be a little more readable.
2023-03-09 13:30:02 +01:00
Nicolas Dorier
9f99dc00cf
Merge pull request #50 from btcpayserver/fewiouq
Update Avalonia, reactivate trimming
2022-07-15 17:11:52 +09:00
nicolas.dorier
2bc9d5b244
Bump
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2022-07-15 17:11:41 +09:00
nicolas.dorier
ed9a3c76eb
Update Avalonia, reactivate trimming 2022-07-15 17:00:48 +09:00
nicolas.dorier
0904f02364
Update Release.md
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2022-07-15 15:49:08 +09:00
nicolas.dorier
1fa32fab04
Turn off IL trimming 2022-07-15 15:47:57 +09:00
nicolas.dorier
df658118b1
Fix versions
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2022-06-20 15:46:08 +09:00
nicolas.dorier
2085d8474d
Fix warning 2022-06-20 15:43:13 +09:00
nicolas.dorier
938e4c8072
Bump to .net6.0 2022-06-20 15:38:21 +09:00
Nicolas Dorier
05b37a286e
Merge pull request #47 from btcpayserver/oqfinw
Add dotnet6.0 runtime deps to debian control file (Fix #45)
2022-06-20 15:24:41 +09:00
nicolas.dorier
c8f27d2331
Add dotnet6.0 runtime deps to debian control file 2022-06-20 15:21:57 +09:00
nicolas.dorier
9cf885ef1d
bump hwi
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2022-05-26 11:28:53 +09:00
nicolas.dorier
41c03b3f97
bump library 2021-09-08 18:21:41 +09:00
nicolas.dorier
80427e7d05
Improve hwi version parsing 2021-09-08 12:00:35 +09:00
nicolas.dorier
661e0db88a
Untype hardware model, update NBitcoin 2021-09-08 11:40:47 +09:00
Pavlenex
d68e99e82e
Merge pull request #37 from dennisreimann/patch-1
Improve PGP check doc
2021-08-19 19:19:14 +02:00
Dennis Reimann
888c75e0eb
Link fixes in README 2021-08-18 15:50:58 +02:00
d11n
d4fb931c47
Improve PGP check doc 2021-08-18 11:54:57 +02:00
nicolas.dorier
dcc25188e0
Use ubuntu for makerelease 2021-05-27 16:26:03 +09:00
nicolas.dorier
bd6e0d0e01
Fix makerelease
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2021-05-27 16:03:53 +09:00
nicolas.dorier
c5fee2613a
Add --no-tty to gpg 2021-05-27 15:46:59 +09:00
nicolas.dorier
c646ddc007
fix hash of rcedit 2021-05-27 15:06:47 +09:00
nicolas.dorier
c7ea938bbb
Fix hash of windows hwi 2021-05-27 15:00:14 +09:00
nicolas.dorier
d14484851e
fix download links for hwi 2021-05-27 14:57:21 +09:00
nicolas.dorier
ec97699fcb
Bump version 2021-05-27 14:53:37 +09:00
nicolas.dorier
7eb8cdb1ff
Update RELEASE.md 2021-05-27 14:51:52 +09:00
nicolas.dorier
6fb2852aa8
Update hashes, remove support for < 2.0.0 2021-05-27 14:51:04 +09:00
nicolas.dorier
e704c9dc96
Update NBitcoin 2021-05-27 14:51:04 +09:00
nicolas.dorier
24619f6233
The Hwi client should assume hwi > 2.0.0. Best effort to keep backward compat with < 2.0.0 clients 2021-05-27 14:51:04 +09:00
nicolas.dorier
d68e15d303
Add 2.0.1 2021-05-27 14:51:04 +09:00
nicolas.dorier
527249be9f
Use ProcessRunner to run command line 2021-05-27 14:51:04 +09:00
Nicolas Dorier
865afd3266
Merge pull request #30 from britttttk/patch-1
Make the verification more clear for Windows & Mac
2021-02-02 16:33:56 +09:00
britttttk
bc4aaa453f
Add how to verify on Mac 2021-02-02 00:27:20 -07:00
britttttk
419fa84396
Make the verification more clear for Windows 2021-02-01 23:57:59 -07:00
Nicolas Dorier
f7675e7605
Update applesign.md 2021-01-28 13:02:50 +09:00
nicolas.dorier
14eee59899
Update the badge README 2020-12-26 00:58:17 +09:00
nicolas.dorier
554b53c0e0
update udev 2020-12-26 00:44:17 +09:00
Pavlenex
b347bbefdd Update issue templates 2020-12-25 12:54:14 +01:00
nicolas.dorier
53663a3f78
Add documentation 2020-12-01 13:45:48 +09:00
nicolas.dorier
4f89b618df
chmod azcopy 2020-12-01 12:51:37 +09:00
nicolas.dorier
c2de25f4bd
Fix CI build on master 2020-12-01 12:42:29 +09:00
nicolas.dorier
acad623520
Fix makerelease script
Some checks failed
CI / debian-x64 (push) Has been cancelled
CI / linux-x64 (push) Has been cancelled
CI / osx-x64 (push) Has been cancelled
CI / win-x64 (push) Has been cancelled
CI / applesign (push) Has been cancelled
CI / pgpsign (push) Has been cancelled
CI / makerelease (push) Has been cancelled
2020-12-01 12:35:40 +09:00
nicolas.dorier
e5342af0e1
bump version 2020-11-30 22:16:02 +09:00
nicolas.dorier
dcad46031d
Fix azcopy in gpgsign step 2020-11-30 14:10:06 +09:00
nicolas.dorier
a7958b5e3e
Bump Avalonia 2020-11-30 13:06:22 +09:00
nicolas.dorier
0f730f6191
Add ability to make a new release for test 2020-11-30 12:43:20 +09:00
nicolas.dorier
c52a3bf004
Move from travis to github actions 2020-11-30 12:40:36 +09:00
nicolas.dorier
3925da235b
Move from travis to github actions 2020-11-27 12:24:53 +09:00
71 changed files with 1511 additions and 685 deletions

View File

@ -0,0 +1,51 @@
---
name: " \U0001F41B Bug report "
about: 'Report a bug or a technical issue '
title: ''
labels: ''
assignees: ''
---
<!--
Thank you for reporting a technical issue.
This issue tracker is only for BTCPay For BTCPay Vault application bug reports and problems. Documentation is available here https://docs.btcpayserver.org/Vault/
Before submitting a bug report please check:
1. Your Hardware wallet is running on latest versions
2. Close any external applications that may be using your wallet (Ledger Live, etc)
3. If you have ,remove any U2F devices such as Yubikey from your PC
4. Try different browser, Brave is known to cause problems with vault.
Please fill in as much of the template below as you're able.
-->
**Describe the bug**
<!--A clear and concise description of what the bug is.-->
**Your BTCPay Environment (please complete the following information):**
- BTCPay Server Version: <!--[available in the right bottom corner of footer] -->
- BTCPay Server Vault app version: <!--[available in the right bottom corner of footer] -->
- Deployment Method: <!--[e.g. Docker, Manual, Third-Party-host]-->
**Your local environment (please complete the following information):**
- Your operating system <!--[e.g. MacOS Catalina 10.15.7, Windows 10..]-->
- Browser: <!--[e.g. Chrome, Safari]-->
**Your hardware wallet details (please complete the following information):**
- Hardware wallet name:
- Hardware wallet version:
- Hardware wallet firmware, bootloader, microcontroller verisons (where applicable):
**Additional context**
<!--Add any other context about the problem here.-->
**Screenshots / Video / GIf (if applicable)**
<!--If your problem is better explained visually, please add a screenshot or record a video.-->

95
.github/workflows/master.yml vendored Normal file
View File

@ -0,0 +1,95 @@
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
tags:
- 'Vault/*'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
AZURE_STORAGE_CONTAINER: dist
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
debian-x64:
name: debian-x64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./Build/CI/build.sh
env:
RID: debian-x64
PGP_KEY: ${{ secrets.PGP_KEY }}
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
linux-x64:
name: linux-x64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./Build/CI/build.sh
env:
RID: linux-x64
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
osx-x64:
name: osx-x64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./Build/CI/build.sh
env:
RID: osx-x64
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
win-x64:
name: win-x64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./Build/CI/build.sh
env:
RID: win-x64
WINDOWS_CERT: ${{ secrets.WINDOWS_CERT }}
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
applesign:
name: applesign
runs-on: macOS-latest
needs: [osx-x64]
if: startsWith( github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4
- run: ./Build/CI/applesign.sh
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_DEV_ID_CERT: ${{ secrets.APPLE_DEV_ID_CERT }}
APPLE_DEV_ID_CERT_PASSWORD: ${{ secrets.APPLE_DEV_ID_CERT_PASSWORD }}
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
pgpsign:
name: pgpsign
runs-on: ubuntu-latest
needs: [win-x64, osx-x64, linux-x64, debian-x64, applesign]
if: ${{ always() }}
steps:
- uses: actions/checkout@v4
- run: ./Build/CI/pgpsign.sh
env:
PGP_KEY: ${{ secrets.PGP_KEY }}
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
makerelease:
name: makerelease
runs-on: ubuntu-latest
needs: [pgpsign, applesign]
if: startsWith( github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4
- run: ./Build/CI/makerelease.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}

View File

@ -1,53 +0,0 @@
# This travis file relis on the following environment variable to configure on your travis repository settings:
# * GITHUB_TOKEN: A github token used to create a new release when a tag is detected (public_repo scope)
# * AZURE_STORAGE_CONTAINER: The azure storage container name to use (can be anything, lowercase, no strange chars)
# * AZURE_STORAGE_CONNECTION_STRING: The azure storage connection string (WARNING your need to enclose it with double quotes "")
# * PGP_KEY: The private PGP key to use to signs all the artifact, in base64 (cat your-pgp.key | base64 -w0)
# * APPLE_DEV_ID_CERT: The p12 file to codesign the dmg file (cat my_cert.p12 | base64 -w0)
# * APPLE_DEV_ID_CERT_PASSWORD: The password of the p12 file
# * APPLE_ID: Your apple id, used by notarization process
# * APPLE_ID_PASSWORD: A app specific password, used by notarization process
# * WINDOWS_CERT: Certificate used to sign windows setup file
# * WINDOWS_CERT_PASSWORD: Password of the certificate
# For more information to setup apple signature settings, see Build/travis/applesign.md
# For more information to setup windows signature settings, see Build/travis/windowssign.md
language: minimal
services:
- docker
stages:
- build
- name: applesign
if: tag IS present
- pgpsign
- name: makerelease
if: tag IS present
jobs:
include:
- stage: build
env: RID=debian-x64
script: |
export DOCKER_BUILD_ARGS="--build-arg "PGP_KEY=$PGP_KEY""
./Build/travis/build.sh
-
env: RID=linux-x64
script: ./Build/travis/build.sh
-
env: RID=osx-x64
script: ./Build/travis/build.sh
-
env: RID=win-x64
script: |
export DOCKER_BUILD_ARGS="--build-arg "WINDOWS_CERT=$WINDOWS_CERT" --build-arg "WINDOWS_CERT_PASSWORD=$WINDOWS_CERT_PASSWORD""
./Build/travis/build.sh
- stage: applesign
os: osx
osx_image: xcode11.2
script: ./Build/travis/applesign.sh
- stage: pgpsign
script: ./Build/travis/pgpsign.sh
- stage: makerelease
script: ./Build/travis/makerelease.sh

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="Version.csproj" Condition="Exists('Version.csproj')" /> <Import Project="Version.csproj" Condition="Exists('Version.csproj')" />
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.1;netcoreapp2.1</TargetFrameworks> <TargetFrameworks>netstandard2.1</TargetFrameworks>
<Company>BTCPay Server</Company> <Company>BTCPay Server</Company>
<Copyright>Copyright © BTCPay Server</Copyright> <Copyright>Copyright © BTCPay Server</Copyright>
<Description>A wrapper library around the hwi bitcoin-core project</Description> <Description>A wrapper library around the hwi bitcoin-core project</Description>
@ -10,19 +10,19 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/btcpayserver/BTCPayServer.Vault</RepositoryUrl> <RepositoryUrl>https://github.com/btcpayserver/BTCPayServer.Vault</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<LangVersion>8.0</LangVersion> <LangVersion>12</LangVersion>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.203" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NBitcoin" Version="5.0.13" /> <PackageReference Include="NBitcoin" Version="10.0.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -49,7 +49,7 @@ namespace BTCPayServer.Hwi.Deployment
process.ArgumentList.Add(directory); process.ArgumentList.Add(directory);
process.ArgumentList.Add("hwi"); process.ArgumentList.Add("hwi");
var extractedPath = Path.Combine(directory, "hwi"); var extractedPath = Path.Combine(directory, "hwi");
Process.Start(process).WaitForExit(); System.Diagnostics.Process.Start(process).WaitForExit();
if (!File.Exists(extractedPath)) if (!File.Exists(extractedPath))
throw new InvalidOperationException($"hwi was not extracted properly to {extractedPath}"); throw new InvalidOperationException($"hwi was not extracted properly to {extractedPath}");
if (extractedPath != outputFileName) if (extractedPath != outputFileName)

View File

@ -15,134 +15,117 @@ namespace BTCPayServer.Hwi.Deployment
{ {
public class HwiVersions public class HwiVersions
{ {
public static HwiVersion v1_0_3 { get; } = new HwiVersion() public static HwiVersion v2_0_1 { get; } = new HwiVersion()
{ {
Windows = new HwiDownloadInfo() Windows = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.0.3/hwi-1.0.3-windows-amd64.zip", Link = "https://github.com/bitcoin-core/HWI/releases/download/2.0.1/hwi-2.0.1-windows-amd64.zip",
Hash = "f52ec4c8dd2dbef4aabe28d8a49580bceb54fd609b84c753d6354eeecbd6dc7a", Hash = "2cfdd6ae51e345f8c70214d626430c8d236336688a87f7d85fc6f3d6a8392da8",
Extractor = new ZipExtractor() Extractor = new ZipExtractor()
}, },
Linux = new HwiDownloadInfo() Linux = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.0.3/hwi-1.0.3-linux-amd64.tar.gz", Link = "https://github.com/bitcoin-core/HWI/releases/download/2.0.1/hwi-2.0.1-linux-amd64.tar.gz",
Hash = "00cb4b2c6eb78d848124e1c3707bdae9c95667f1397dd32cf3b51b579b3a010a", Hash = "ca1f91593b3c0a99269ecbc0f85aced08e2dec4bf263cfb25429e047e63e38d5",
Extractor = new TarExtractor() Extractor = new TarExtractor()
}, },
Mac = new HwiDownloadInfo() Mac = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.0.3/hwi-1.0.3-mac-amd64.tar.gz", Link = "https://github.com/bitcoin-core/HWI/releases/download/2.0.1/hwi-2.0.1-mac-amd64.tar.gz",
Hash = "b0219f4f51d74e4525dd57a19f1aee9df409a879e041ea65f2d70cf90ac48a32", Hash = "389afc3927cbc6ce01f464d8d6fa66bf050d2b7d17d7127d1c1e6ee89c5b5ec1",
Extractor = new TarExtractor()
}
};
public static HwiVersion v2_1_1 { get; } = new HwiVersion()
{
Windows = new HwiDownloadInfo()
{
Link = "https://github.com/bitcoin-core/HWI/releases/download/2.1.1/hwi-2.1.1-windows-amd64.zip",
Hash = "3efa5bcde386ca5523a4127f3a9802a7e9ef5320c2a8910ead343386c0b7dbfc",
Extractor = new ZipExtractor()
},
Linux = new HwiDownloadInfo()
{
Link = "https://github.com/bitcoin-core/HWI/releases/download/2.1.1/hwi-2.1.1-linux-amd64.tar.gz",
Hash = "7f4cbe4e5c2cd1ac892f9bd8ac35fb1f837b6a547b528b61aca895a212a90062",
Extractor = new TarExtractor()
},
Mac = new HwiDownloadInfo()
{
Link = "https://github.com/bitcoin-core/HWI/releases/download/2.1.1/hwi-2.1.1-mac-amd64.tar.gz",
Hash = "1b1a903b4a9884aa06593356e7a958c19ccb56a5bc97e0c6075f968310640fd2",
Extractor = new TarExtractor() Extractor = new TarExtractor()
} }
}; };
public static HwiVersion v1_1_0 { get; } = new HwiVersion() public static HwiVersion v2_3_1 { get; } = new HwiVersion()
{ {
Windows = new HwiDownloadInfo() Windows = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.1.0/hwi-1.1.0-windows-amd64.zip", Link = "https://github.com/bitcoin-core/HWI/releases/download/2.3.1/hwi-2.3.1-windows-x86_64.zip",
Hash = "cabf83aad91c44c78f6830c31309b9cfa61b900d27c1beb5ee07152e66167853", Hash = "460c8b83a9d8888ad769ffdc34dbe3ad7ecd27b22035494bdeb268d943be1791",
Extractor = new ZipExtractor() Extractor = new ZipExtractor()
}, },
Linux = new HwiDownloadInfo() Linux = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.1.0/hwi-1.1.0-linux-amd64.tar.gz", Link = "https://github.com/bitcoin-core/HWI/releases/download/2.3.1/hwi-2.3.1-linux-x86_64.tar.gz",
Hash = "1e98a59ee0b99ccac7ec6a62e55bf9fa88650250009aecba50fd10468031ed01", Hash = "9519023b3a485b68668675db8ab70be2e338be100fd2731eeddd6d34fc440580",
Extractor = new TarExtractor() Extractor = new TarExtractor()
}, },
Mac = new HwiDownloadInfo() Mac = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.1.0/hwi-1.1.0-mac-amd64.tar.gz", Link = "https://github.com/bitcoin-core/HWI/releases/download/2.3.1/hwi-2.3.1-hwi-2.3.1-mac-x86_64.tar.gz",
Hash = "195d61bb941b6e2e6aab229f16a039f207407f80e628297b8a0cb85228e754ea", Hash = "9059b8f7cf6fe42f6e37cd8015cd11cb8fb736650797b25da849c625ed61ea62",
Extractor = new TarExtractor() Extractor = new TarExtractor()
} }
}; };
public static HwiVersion v1_1_1 { get; } = new HwiVersion()
public static HwiVersion v3_0_0 { get; } = new HwiVersion()
{ {
Windows = new HwiDownloadInfo() Windows = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.1.1/hwi-1.1.1-windows-amd64.zip", Link = "https://github.com/bitcoin-core/HWI/releases/download/3.0.0/hwi-3.0.0-windows-x86_64.zip",
Hash = "c36bd39635097c4fa952aceca3f4c7c74be2035a31c39a10a33dae53996630aa", Hash = "38b3f02374c300516b4583a1195ffe1cac1159f9885b8ab434fd450e290c907a",
Extractor = new ZipExtractor() Extractor = new ZipExtractor()
}, },
Linux = new HwiDownloadInfo() Linux = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.1.1/hwi-1.1.1-linux-amd64.tar.gz", Link = "https://github.com/bitcoin-core/HWI/releases/download/3.0.0/hwi-3.0.0-linux-x86_64.tar.gz",
Hash = "e786797701e454415ed170ee9aed4c81a33f1adef6821bb4bd0f92d1df9d3b23", Hash = "9b70aab37a1265457de4aaa242bd24a0abef5056357d8337bd79232e9b85bc1c",
Extractor = new TarExtractor() Extractor = new TarExtractor()
}, },
Mac = new HwiDownloadInfo() Mac = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.1.1/hwi-1.1.1-mac-amd64.tar.gz", Link = "https://github.com/bitcoin-core/HWI/releases/download/3.0.0/hwi-3.0.0-mac-x86_64.tar.gz",
Hash = "1f48ac21c42579aa88c98e02571ed4d2dfa48f973cd6904984bc9a8b304816ad", Hash = "d05c046d5718bf92b348a786aad15cb0f0132fcccf57a646758610240327a977",
Extractor = new TarExtractor() Extractor = new TarExtractor()
} }
}; };
public static HwiVersion v1_1_2 { get; } = new HwiVersion()
public static HwiVersion v3_2_0 { get; } = new HwiVersion()
{ {
Version = "3.2.0",
Windows = new HwiDownloadInfo() Windows = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.1.2/hwi-1.1.2-windows-amd64.zip", Link = "https://github.com/bitcoin-core/HWI/releases/download/{0}/hwi-{0}-windows-x86_64.zip",
Hash = "0f3fb7c89740ac2cf245bb8e743c5dd7e686efbda8c4a288869621a63bc32ced", Hash = "e068d91b664597425a8ead02d7b86a02ad6c4b72746c42961f58a58b08f9fd79",
Extractor = new ZipExtractor() Extractor = new ZipExtractor()
}, },
Linux = new HwiDownloadInfo() Linux = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.1.2/hwi-1.1.2-linux-amd64.tar.gz", Link = "https://github.com/bitcoin-core/HWI/releases/download/{0}/hwi-{0}-linux-x86_64.tar.gz",
Hash = "fd6cca20aaa24f4ae4332ca01f1d4c2711247e3ccb8bbea44ee93456f211ea4b", Hash = "d9cc65de95e3cf93fd3c953d589184a00180624ffc5ad17aade97616a8919fa6",
Extractor = new TarExtractor() Extractor = new TarExtractor()
}, },
Mac = new HwiDownloadInfo() Mac = new HwiDownloadInfo()
{ {
Link = "https://github.com/bitcoin-core/HWI/releases/download/1.1.2/hwi-1.1.2-mac-amd64.tar.gz", Link = "https://github.com/bitcoin-core/HWI/releases/download/{0}/hwi-{0}-mac-x86_64.tar.gz",
Hash = "630aef7a02cbc08fae95e79bb9c01684650426a6f8e5383cfb040093b05aa0f1", Hash = "b3764a530b635e7a7348c9185e09e74b389f5f585094fe316f700eec7c761875",
Extractor = new TarExtractor() Extractor = new TarExtractor()
} }
}; };
public static HwiVersion v1_2_0 { get; } = new HwiVersion()
{ public static HwiVersion Latest => v3_2_0;
Windows = new HwiDownloadInfo()
{
Link = "https://github.com/bitcoin-core/HWI/releases/download/1_2_0/hwi-1_2_0-windows-amd64.zip",
Hash = "599dde27eb97cf48d9fe6395e1158cc471bdf6168228facbb9d7090ce9e14634",
Extractor = new ZipExtractor()
},
Linux = new HwiDownloadInfo()
{
Link = "https://github.com/bitcoin-core/HWI/releases/download/1_2_0/hwi-1_2_0-linux-amd64.tar.gz",
Hash = "92c263bd2e5c41a533972900e856e0ee9a004ad507024b38462c69afae361cea",
Extractor = new TarExtractor()
},
Mac = new HwiDownloadInfo()
{
Link = "https://github.com/bitcoin-core/HWI/releases/download/1_2_0/hwi-1_2_0-mac-amd64.tar.gz",
Hash = "96437674a1bec7ee87aced6f429c9adcf74a749f41f3355cf1d5adb859fa4304",
Extractor = new TarExtractor()
}
};
public static HwiVersion v1_2_1 { get; } = new HwiVersion()
{
Windows = new HwiDownloadInfo()
{
Link = "https://github.com/bitcoin-core/HWI/releases/download/1_2_1/hwi-1_2_1-windows-amd64.zip",
Hash = "b8b21499592a311cfaa18676280807d6bf674d72cef21409ed265069f6582c1b",
Extractor = new ZipExtractor()
},
Linux = new HwiDownloadInfo()
{
Link = "https://github.com/bitcoin-core/HWI/releases/download/1_2_1/hwi-1_2_1-linux-amd64.tar.gz",
Hash = "23ea301117f74561294b5b3ebe1eeb461004aff7e479c4b90a0aaec5924cc677",
Extractor = new TarExtractor()
},
Mac = new HwiDownloadInfo()
{
Link = "https://github.com/bitcoin-core/HWI/releases/download/1_2_1/hwi-1_2_1-mac-amd64.tar.gz",
Hash = "dc516e563db7c0f21b3f017313fc93a2a57f8d614822b8c71f1467a4e5f59dbb",
Extractor = new TarExtractor()
}
};
public static HwiVersion Latest => v1_2_1;
} }
public class HwiVersion public class HwiVersion
@ -160,6 +143,8 @@ namespace BTCPayServer.Hwi.Deployment
throw new NotSupportedException(); throw new NotSupportedException();
} }
} }
public string Version { get; set; }
} }
public class HwiDownloadInfo public class HwiDownloadInfo
@ -174,7 +159,7 @@ namespace BTCPayServer.Hwi.Deployment
using (var stream = File.Open(processName, FileMode.Open, FileAccess.Read)) using (var stream = File.Open(processName, FileMode.Open, FileAccess.Read))
using (var bufferedStream = new BufferedStream(stream, 1024 * 32)) using (var bufferedStream = new BufferedStream(stream, 1024 * 32))
{ {
var sha = new SHA256Managed(); var sha = SHA256.Create();
checksum = sha.ComputeHash(bufferedStream); checksum = sha.ComputeHash(bufferedStream);
} }
return Encoders.Hex.EncodeData(checksum); return Encoders.Hex.EncodeData(checksum);
@ -195,8 +180,9 @@ namespace BTCPayServer.Hwi.Deployment
download: download:
if (!File.Exists(processFullPath)) if (!File.Exists(processFullPath))
{ {
var data = await HttpClient.GetStreamAsync(Link); var link = HwiVersions.Latest.Version is null ? Link : Link.Replace("{0}", HwiVersions.Latest.Version);
var downloadedFile = Path.Combine(destinationDirectory, Link.Split('/').Last()); var data = await HttpClient.GetStreamAsync(link);
var downloadedFile = Path.Combine(destinationDirectory, link.Split('/').Last());
try try
{ {
using (var fs = File.Open(downloadedFile, FileMode.Create, FileAccess.ReadWrite)) using (var fs = File.Open(downloadedFile, FileMode.Create, FileAccess.ReadWrite))

View File

@ -53,12 +53,12 @@ namespace BTCPayServer.Hwi
w.Add(devicePath); w.Add(devicePath);
}); });
} }
public static DeviceSelector FromDeviceType(HardwareWalletModels deviceType, string devicePath = null) public static DeviceSelector FromDeviceType(string deviceType, string devicePath = null)
{ {
return new LambdaDeviceSelector(w => return new LambdaDeviceSelector(w =>
{ {
w.Add($"--device-type"); w.Add($"--device-type");
w.Add(deviceType.ToHwiFriendlyString()); w.Add(deviceType.ToString().ToLowerInvariant());
if (!string.IsNullOrEmpty(devicePath)) if (!string.IsNullOrEmpty(devicePath))
{ {
w.Add($"--device-path"); w.Add($"--device-path");

View File

@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Hwi
{
/// <summary>
/// https://github.com/bitcoin-core/HWI/pull/228
/// </summary>
public enum HardwareWalletModels
{
Unknown,
Coldcard,
Coldcard_Simulator,
DigitalBitBox_01,
DigitalBitBox_01_Simulator,
KeepKey,
KeepKey_Simulator,
Ledger_Nano_S,
Trezor_1,
Trezor_1_Simulator,
Trezor_T,
Trezor_T_Simulator,
}
}

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Helpers; using BTCPayServer.Helpers;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -12,7 +13,7 @@ namespace BTCPayServer.Hwi
{ {
public class HwiDeviceClient public class HwiDeviceClient
{ {
public HwiDeviceClient(HwiClient hwiClient, DeviceSelector deviceSelector, HardwareWalletModels model, HDFingerprint? fingerprint) public HwiDeviceClient(HwiClient hwiClient, DeviceSelector deviceSelector, string model, HDFingerprint? fingerprint)
{ {
HwiClient = hwiClient ?? throw new ArgumentNullException(nameof(hwiClient)); HwiClient = hwiClient ?? throw new ArgumentNullException(nameof(hwiClient));
DeviceSelector = deviceSelector ?? throw new ArgumentNullException(nameof(deviceSelector)); DeviceSelector = deviceSelector ?? throw new ArgumentNullException(nameof(deviceSelector));
@ -26,7 +27,7 @@ namespace BTCPayServer.Hwi
public string Password { get; set; } public string Password { get; set; }
public HwiClient HwiClient { get; } public HwiClient HwiClient { get; }
public DeviceSelector DeviceSelector { get; } public DeviceSelector DeviceSelector { get; }
public HardwareWalletModels Model { get; } public string Model { get; }
public HDFingerprint? Fingerprint { get; } public HDFingerprint? Fingerprint { get; }
public Task PromptPinAsync(CancellationToken cancellationToken = default) public Task PromptPinAsync(CancellationToken cancellationToken = default)
@ -93,21 +94,27 @@ namespace BTCPayServer.Hwi
return NBitcoinHelpers.BetterParseExtPubKey(extPubKeyString, this.HwiClient.Network, HwiClient.IgnoreInvalidNetwork); return NBitcoinHelpers.BetterParseExtPubKey(extPubKeyString, this.HwiClient.Network, HwiClient.IgnoreInvalidNetwork);
} }
public async Task DisplayAddressAsync(ScriptPubKeyType addressType, KeyPath keyPath, CancellationToken cancellationToken = default) public async Task<BitcoinAddress> DisplayAddressAsync(ScriptPubKeyType addressType, KeyPath keyPath, CancellationToken cancellationToken = default)
{ {
if (keyPath == null) if (keyPath == null)
throw new ArgumentNullException(nameof(keyPath)); throw new ArgumentNullException(nameof(keyPath));
List<string> commandArguments = new List<string>(); List<string> commandArguments = new List<string>();
commandArguments.Add("--path"); commandArguments.Add("--path");
commandArguments.Add(keyPath.ToString(true, "h")); commandArguments.Add(keyPath.ToString(true, "h"));
commandArguments.Add("--addr-type");
switch (addressType) switch (addressType)
{ {
case ScriptPubKeyType.Legacy:
commandArguments.Add("legacy");
break;
case ScriptPubKeyType.Segwit: case ScriptPubKeyType.Segwit:
commandArguments.Add("--wpkh"); commandArguments.Add("wit");
break; break;
case ScriptPubKeyType.SegwitP2SH: case ScriptPubKeyType.SegwitP2SH:
commandArguments.Add("--sh_wpkh"); commandArguments.Add("sh_wit");
break;
case ScriptPubKeyType.TaprootBIP86:
commandArguments.Add("tap");
break; break;
} }
@ -116,10 +123,55 @@ namespace BTCPayServer.Hwi
commandArguments: commandArguments.ToArray(), commandArguments: commandArguments.ToArray(),
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
if (!HwiClient.IgnoreInvalidNetwork) return ParseAddress(response, HwiClient.Network, HwiClient.IgnoreInvalidNetwork);
HwiParser.ParseAddress(response, HwiClient.Network);
} }
private static BitcoinAddress ParseAddress(string response, Network expectedNetwork, bool ignoreInvalidNetwork)
{
if (JsonHelpers.TryParseJToken(response, out JToken token) &&
token["address"]?.ToString()?.Trim() is String address)
{
try
{
return BitcoinAddress.Create(address, expectedNetwork);
}
catch when (ignoreInvalidNetwork)
{
var set = expectedNetwork.NetworkSet;
// Some wallet does not really support --chain parameter. So we need to bruteforce the proper format
foreach (var network in new[]
{
set.Mainnet,
set.Testnet,
set.Regtest,
set.GetNetwork(new ChainName("Signet"))
})
{
if (network is null)
continue;
if (network == expectedNetwork)
continue;
try
{
return BitcoinAddress.Create(address, network).ToNetwork(expectedNetwork);
}
catch
{
}
}
throw new FormatException(CantParseAddress);
}
catch (Exception ex)
{
throw new FormatException(CantParseAddress, ex);
}
}
throw new FormatException(CantParseAddress);
}
const string CantParseAddress = "The device returned an address which can't be parsed. Please use HwiClient.IgnoreInvalidNetwork=true to ignore.";
public async Task<PSBT> SignPSBTAsync(PSBT psbt, CancellationToken cancellationToken = default) public async Task<PSBT> SignPSBTAsync(PSBT psbt, CancellationToken cancellationToken = default)
{ {
if (psbt == null) if (psbt == null)

View File

@ -8,7 +8,7 @@ namespace BTCPayServer.Hwi
{ {
public class HwiEnumerateEntry public class HwiEnumerateEntry
{ {
public HardwareWalletModels Model { get; } public string Model { get; }
public string Path { get; } public string Path { get; }
public string SerialNumber { get; } public string SerialNumber { get; }
public HDFingerprint? Fingerprint { get; } public HDFingerprint? Fingerprint { get; }
@ -16,6 +16,7 @@ namespace BTCPayServer.Hwi
public bool? NeedsPassphraseSent { get; } public bool? NeedsPassphraseSent { get; }
public string Error { get; } public string Error { get; }
public HwiErrorCode? Code { get; } public HwiErrorCode? Code { get; }
public JObject RawData { get; }
public DeviceSelector DeviceSelector { get; } public DeviceSelector DeviceSelector { get; }
@ -27,14 +28,15 @@ namespace BTCPayServer.Hwi
} }
public HwiEnumerateEntry( public HwiEnumerateEntry(
HardwareWalletModels model, string model,
string path, string path,
string serialNumber, string serialNumber,
HDFingerprint? fingerprint, HDFingerprint? fingerprint,
bool? needsPinSent, bool? needsPinSent,
bool? needsPassphraseSent, bool? needsPassphraseSent,
string error, string error,
HwiErrorCode? code) HwiErrorCode? code,
JObject rawData)
{ {
Model = model; Model = model;
Path = path; Path = path;
@ -44,6 +46,7 @@ namespace BTCPayServer.Hwi
NeedsPassphraseSent = needsPassphraseSent; NeedsPassphraseSent = needsPassphraseSent;
Error = error; Error = error;
Code = code; Code = code;
RawData = rawData;
DeviceSelector = fingerprint is HDFingerprint fp ? DeviceSelectors.FromFingerprint(fp) : DeviceSelector = fingerprint is HDFingerprint fp ? DeviceSelectors.FromFingerprint(fp) :
DeviceSelectors.FromDeviceType(Model, path); DeviceSelectors.FromDeviceType(Model, path);
} }

View File

@ -15,7 +15,6 @@ namespace BTCPayServer.Hwi
public static HwiOption Password(string password) => new HwiOption(HwiOptions.Password, password); public static HwiOption Password(string password) => new HwiOption(HwiOptions.Password, password);
public static HwiOption TestNet => new HwiOption(HwiOptions.TestNet);
public static HwiOption Version => new HwiOption(HwiOptions.Version); public static HwiOption Version => new HwiOption(HwiOptions.Version);
private HwiOption(HwiOptions type, string argument = null) private HwiOption(HwiOptions type, string argument = null)

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using BTCPayServer.Helpers; using BTCPayServer.Helpers;
using System.Text.RegularExpressions;
namespace BTCPayServer.Hwi namespace BTCPayServer.Hwi
{ {
@ -104,32 +105,6 @@ namespace BTCPayServer.Hwi
return false; return false;
} }
public static bool TryParseHardwareWalletVendor(JToken token, out HardwareWalletModels vendor)
{
vendor = HardwareWalletModels.Unknown;
if (token is null)
{
return false;
}
try
{
var typeString = token.Value<string>();
if (Enum.TryParse(typeString, ignoreCase: true, out HardwareWalletModels t))
{
vendor = t;
return true;
}
}
catch
{
return false;
}
return false;
}
public static IEnumerable<HwiEnumerateEntry> ParseHwiEnumerateResponse(string responseString) public static IEnumerable<HwiEnumerateEntry> ParseHwiEnumerateResponse(string responseString)
{ {
var jarr = JArray.Parse(responseString); var jarr = JArray.Parse(responseString);
@ -144,35 +119,6 @@ namespace BTCPayServer.Hwi
return response; return response;
} }
public static BitcoinAddress ParseAddress(string json, Network network)
{
var expectedNetwork = network;
// HWI does not support regtest, so the parsing would fail here.
if (network.NetworkType == NetworkType.Regtest)
{
network = network.NetworkSet.Testnet;
}
if (JsonHelpers.TryParseJToken(json, out JToken token))
{
var addressString = token["address"]?.ToString()?.Trim() ?? null;
try
{
var address = BitcoinAddress.Create(addressString, network);
return address.ToNetwork(expectedNetwork);
}
catch (FormatException)
{
BitcoinAddress.Create(addressString, network.NetworkType == NetworkType.Mainnet ? network.NetworkSet.Testnet : network.NetworkSet.Mainnet);
throw new FormatException("Wrong network.");
}
}
else
{
throw new FormatException($"Could not parse address: {json}.");
}
}
public static PSBT ParsePsbt(string json, Network network) public static PSBT ParsePsbt(string json, Network network)
{ {
// HWI does not support regtest, so the parsing would fail here. // HWI does not support regtest, so the parsing would fail here.
@ -195,7 +141,7 @@ namespace BTCPayServer.Hwi
public static HwiEnumerateEntry ParseHwiEnumerateEntry(JObject json) public static HwiEnumerateEntry ParseHwiEnumerateEntry(JObject json)
{ {
JToken modelToken = json["model"]; string model = json["model"]?.Value<string>();
var pathString = json["path"]?.ToString()?.Trim(); var pathString = json["path"]?.ToString()?.Trim();
var serialNumberString = json["serial_number"]?.ToString()?.Trim(); var serialNumberString = json["serial_number"]?.ToString()?.Trim();
var fingerprintString = json["fingerprint"]?.ToString()?.Trim(); var fingerprintString = json["fingerprint"]?.ToString()?.Trim();
@ -235,12 +181,6 @@ namespace BTCPayServer.Hwi
errorString = err.Message; errorString = err.Message;
} }
HardwareWalletModels model = HardwareWalletModels.Unknown;
if (TryParseHardwareWalletVendor(modelToken, out HardwareWalletModels t))
{
model = t;
}
return new HwiEnumerateEntry( return new HwiEnumerateEntry(
model: model, model: model,
path: pathString, path: pathString,
@ -249,7 +189,8 @@ namespace BTCPayServer.Hwi
needsPinSent: needsPinSent, needsPinSent: needsPinSent,
needsPassphraseSent: needsPassphraseSent, needsPassphraseSent: needsPassphraseSent,
error: errorString, error: errorString,
code: code); code: code,
json);
} }
public static string NormalizeRawDevicePath(string rawPath) public static string NormalizeRawDevicePath(string rawPath)
@ -261,39 +202,18 @@ namespace BTCPayServer.Hwi
return rawPath.Replace(@"\\", @"\"); return rawPath.Replace(@"\\", @"\");
} }
public static bool TryParseVersion(string hwiResponse, string substringFrom, out Version version) static Regex VersionRegex = new Regex(@"(\d+)\.(\d+)(\.(\d+))?");
{
int startIndex = hwiResponse.IndexOf(substringFrom) + substringFrom.Length;
var versionString = hwiResponse.Substring(startIndex).Trim();
version = null;
if (Version.TryParse(versionString, out Version v))
{
version = v;
return true;
}
return false;
}
public static bool TryParseVersion(string hwiResponse, out Version version) public static bool TryParseVersion(string hwiResponse, out Version version)
{ {
version = null; version = null;
var m = VersionRegex.Match(hwiResponse);
// Order matters! https://github.com/zkSNACKs/WalletWasabi/pull/1905/commits/cecefcc50af140cc06cb93961cda86f9b21db11b if (!m.Success || m.Groups.Count < 5)
return false;
// Example output: hwi.exe 1.0.1 int.TryParse(m.Groups[1].Value, out var major);
if (TryParseVersion(hwiResponse, "hwi.exe", out Version v2)) int.TryParse(m.Groups[2].Value, out var minor);
{ int.TryParse(m.Groups[4].Value, out var build);
version = v2; version = new Version(major, minor, build);
} return true;
// Example output: hwi 1.0.1
if (TryParseVersion(hwiResponse, "hwi", out Version v1))
{
version = v1;
}
return version != null;
} }
public static Version ParseVersion(string hwiResponse) public static Version ParseVersion(string hwiResponse)
@ -305,17 +225,23 @@ namespace BTCPayServer.Hwi
throw new FormatException($"Cannot parse version from HWI's response. Response: {hwiResponse}."); throw new FormatException($"Cannot parse version from HWI's response. Response: {hwiResponse}.");
} }
static Dictionary<ChainName, string> chainNames = new Dictionary<ChainName, string>()
internal static string[] ToArgumentString(DeviceSelector deviceSelector, Network network, IEnumerable<HwiOption> options, HwiCommands? command, string[] commandArguments) {
{ ChainName.Mainnet, "main" },
{ ChainName.Testnet, "test" },
{ ChainName.Regtest, "regtest" },
{ Bitcoin.Instance.Signet.ChainName, "signet" },
};
internal static string[] ToArgumentString(DeviceSelector deviceSelector, Network network, IEnumerable<HwiOption> options, HwiCommands? command, string[] commandArguments)
{ {
List<string> arguments = new List<string>(); List<string> arguments = new List<string>();
options ??= Enumerable.Empty<HwiOption>(); options ??= Enumerable.Empty<HwiOption>();
var fullOptions = new List<HwiOption>(options); var fullOptions = new List<HwiOption>(options);
if (network.NetworkType != NetworkType.Mainnet) chainNames.TryGetValue(network.ChainName, out var val);
{ val ??= network.ChainName.ToString().ToLowerInvariant();
fullOptions.Insert(0, HwiOption.TestNet); arguments.Add("--chain");
} arguments.Add(val);
foreach (var option in fullOptions) foreach (var option in fullOptions)
{ {
@ -339,10 +265,5 @@ namespace BTCPayServer.Hwi
} }
return arguments.ToArray(); return arguments.ToArray();
} }
public static string ToHwiFriendlyString(this HardwareWalletModels me)
{
return me.ToString().ToLowerInvariant();
}
} }
} }

View File

@ -0,0 +1,301 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// COPIED FROM https://github.com/dotnet/sdk/blob/main/src/BuiltInTools/dotnet-watch/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Hwi.Process
{
internal interface IReporter
{
void Verbose(string message);
void Output(string message);
void Warn(string message);
void Error(string message);
}
internal class NullReporter : IReporter
{
public readonly static NullReporter Instance = new NullReporter();
public void Error(string message)
{
}
public void Output(string message)
{
}
public void Verbose(string message)
{
}
public void Warn(string message)
{
}
}
internal class ProcessRunner
{
private readonly IReporter _reporter;
public ProcessRunner() : this(null)
{
}
public ProcessRunner(IReporter reporter)
{
_reporter = reporter ?? NullReporter.Instance;
}
// May not be necessary in the future. See https://github.com/dotnet/corefx/issues/12039
public async Task<int> RunAsync(ProcessSpec processSpec, CancellationToken cancellationToken)
{
if (processSpec == null)
throw new ArgumentNullException(nameof(processSpec));
int exitCode;
var stopwatch = new Stopwatch();
using (var process = CreateProcess(processSpec))
using (var processState = new ProcessState(process, _reporter))
{
cancellationToken.Register(() => processState.TryKill());
var readOutput = false;
var readError = false;
if (processSpec.IsErrorCaptured)
{
readError = true;
process.ErrorDataReceived += (_, a) =>
{
if (!string.IsNullOrEmpty(a.Data))
{
processSpec.ErrorCapture.AddLine(a.Data);
}
};
}
if (processSpec.IsOutputCaptured)
{
readOutput = true;
process.OutputDataReceived += (_, a) =>
{
if (!string.IsNullOrEmpty(a.Data))
{
processSpec.OutputCapture.AddLine(a.Data);
}
};
}
stopwatch.Start();
process.Start();
_reporter.Verbose($"Started '{processSpec.Executable}' '{process.StartInfo.Arguments}' with process id {process.Id}");
if (readOutput)
{
process.BeginOutputReadLine();
}
if (readError)
{
process.BeginErrorReadLine();
}
if (processSpec.Stdin is not null)
{
foreach (var l in processSpec.Stdin)
process.StandardInput.WriteLine(l);
process.StandardInput.Close();
}
await processState.Task;
exitCode = process.ExitCode;
stopwatch.Stop();
_reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms");
}
return exitCode;
}
private System.Diagnostics.Process CreateProcess(ProcessSpec processSpec)
{
var process = new System.Diagnostics.Process
{
EnableRaisingEvents = true,
StartInfo =
{
FileName = processSpec.Executable,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = processSpec.WorkingDirectory,
RedirectStandardOutput = processSpec.IsOutputCaptured,
RedirectStandardError = processSpec.IsErrorCaptured,
RedirectStandardInput = processSpec.Stdin is not null
}
};
if (!(processSpec.EscapedArguments is null))
{
process.StartInfo.Arguments = processSpec.EscapedArguments;
}
else
{
for (var i = 0; i < processSpec.Arguments.Count; i++)
{
process.StartInfo.ArgumentList.Add(processSpec.Arguments[i]);
}
}
foreach (var env in processSpec.EnvironmentVariables)
{
process.StartInfo.Environment.Add(env.Key, env.Value);
}
SetEnvironmentVariable(process.StartInfo, "DOTNET_STARTUP_HOOKS", processSpec.EnvironmentVariables.DotNetStartupHooks, Path.PathSeparator);
SetEnvironmentVariable(process.StartInfo, "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", processSpec.EnvironmentVariables.AspNetCoreHostingStartupAssemblies, ';');
return process;
}
private void SetEnvironmentVariable(ProcessStartInfo processStartInfo, string envVarName, List<string> envVarValues, char separator)
{
if (envVarValues is { Count: 0 })
{
return;
}
var existing = Environment.GetEnvironmentVariable(envVarName);
string result;
if (!string.IsNullOrEmpty(existing))
{
result = existing + separator + string.Join(separator, envVarValues);
}
else
{
result = string.Join(separator, envVarValues);
}
processStartInfo.EnvironmentVariables[envVarName] = result;
}
private class ProcessState : IDisposable
{
private readonly IReporter _reporter;
private readonly System.Diagnostics.Process _process;
private readonly TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
private volatile bool _disposed;
public ProcessState(System.Diagnostics.Process process, IReporter reporter)
{
_reporter = reporter;
_process = process;
_process.Exited += OnExited;
Task = _tcs.Task.ContinueWith(_ =>
{
try
{
// We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously
// this code used Process.Exited, which could result in us missing some output due to the ordering of
// events.
//
// See the remarks here: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit#System_Diagnostics_Process_WaitForExit_System_Int32_
if (!_process.WaitForExit(Int32.MaxValue))
{
throw new TimeoutException();
}
_process.WaitForExit();
}
catch (InvalidOperationException)
{
// suppress if this throws if no process is associated with this object anymore.
}
});
}
public Task Task { get; }
public void TryKill()
{
if (_disposed)
{
return;
}
try
{
if (!(_process is null) && !_process.HasExited)
{
_reporter.Verbose($"Killing process {_process.Id}");
_process.Kill();
}
}
catch (Exception ex)
{
_reporter.Verbose($"Error while killing process '{_process.StartInfo.FileName} {_process.StartInfo.Arguments}': {ex.Message}");
#if DEBUG
_reporter.Verbose(ex.ToString());
#endif
}
}
private void OnExited(object sender, EventArgs args)
=> _tcs.TrySetResult(null);
public void Dispose()
{
if (!_disposed)
{
TryKill();
_disposed = true;
_process.Exited -= OnExited;
_process.Dispose();
}
}
}
}
internal class ProcessSpec
{
public string Executable { get; set; }
public string WorkingDirectory { get; set; }
public ProcessSpecEnvironmentVariables EnvironmentVariables { get; } = new ProcessSpecEnvironmentVariables();
public IReadOnlyList<string> Arguments { get; set; }
public string EscapedArguments { get; set; }
public OutputCapture OutputCapture { get; set; }
public OutputCapture ErrorCapture { get; set; }
public string ShortDisplayName()
=> Path.GetFileNameWithoutExtension(Executable);
public bool IsOutputCaptured => OutputCapture != null;
public bool IsErrorCaptured => ErrorCapture != null;
public CancellationToken CancelOutputCapture { get; set; }
public string[] Stdin { get; set; }
public sealed class ProcessSpecEnvironmentVariables : Dictionary<string, string>
{
public List<string> DotNetStartupHooks { get; } = new List<string>();
public List<string> AspNetCoreHostingStartupAssemblies { get; } = new List<string>();
}
}
internal class OutputCapture
{
private readonly List<string> _lines = new List<string>();
public IEnumerable<string> Lines => _lines;
public void AddLine(string line) => _lines.Add(line);
}
}

View File

@ -9,6 +9,10 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin.Logging; using NBitcoin.Logging;
using Microsoft.Extensions; using Microsoft.Extensions;
using BTCPayServer.Hwi.Process;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace BTCPayServer.Hwi.Transports namespace BTCPayServer.Hwi.Transports
{ {
@ -29,7 +33,6 @@ namespace BTCPayServer.Hwi.Transports
public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel) public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel)
{ {
string responseString; string responseString;
int exitCode;
var fileName = Path.Combine(hwiFolder, "hwi"); var fileName = Path.Combine(hwiFolder, "hwi");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
!fileName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) !fileName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
@ -37,25 +40,26 @@ namespace BTCPayServer.Hwi.Transports
fileName += ".exe"; fileName += ".exe";
} }
ProcessStartInfo startInfo = new ProcessStartInfo ProcessSpec processSpec = new ProcessSpec()
{ {
FileName = fileName, Executable = fileName,
RedirectStandardOutput = true, OutputCapture = new OutputCapture(),
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
}; };
if (arguments.Contains("signtx", StringComparer.OrdinalIgnoreCase))
{
processSpec.Arguments = new ReadOnlyCollection<string>(new string[] { "--stdin" });
processSpec.Stdin = arguments.Concat(new[] { string.Empty }).ToArray();
}
else
{
processSpec.Arguments = new ReadOnlyCollection<string>(arguments);
}
foreach (var arg in arguments)
startInfo.ArgumentList.Add(arg);
Process process = null;
try try
{ {
process = await StartProcess(startInfo, cancel); ProcessRunner processRunner = new ProcessRunner();
var exitCode = await processRunner.RunAsync(processSpec, cancel);
exitCode = process.ExitCode; responseString = string.Concat(processSpec.OutputCapture.Lines);
responseString = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
Logger.LogDebug($"Exit code: exit code: {exitCode}, Output: {responseString}"); Logger.LogDebug($"Exit code: exit code: {exitCode}, Output: {responseString}");
} }
catch (Exception ex) catch (Exception ex)
@ -63,37 +67,7 @@ namespace BTCPayServer.Hwi.Transports
Logger.LogError(default, ex, "Failed to call hwi"); Logger.LogError(default, ex, "Failed to call hwi");
throw; throw;
} }
finally
{
try
{
if (!process.HasExited)
process.Kill();
}
catch { }
finally { process?.Dispose(); }
}
return responseString; return responseString;
} }
private async Task<Process> StartProcess(ProcessStartInfo startInfo, CancellationToken cancel)
{
await _SemaphoreSlim.WaitAsync(cancel).ConfigureAwait(false);
try
{
if (Logger.IsEnabled(LogLevel.Debug))
{
Logger.LogDebug($"{startInfo.FileName} {string.Join(' ', startInfo.ArgumentList)}");
}
Process process = Process.Start(startInfo);
await process.WaitForExitAsync(cancel).ConfigureAwait(false);
return process;
}
finally
{
_SemaphoreSlim.Release();
}
}
} }
} }

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Hwi.Transports
{
/// <summary>
/// Attempt to modify the args from legacy clients so hwi does not error
/// </summary>
public class LegacyCompatibilityTransport : ITransport
{
public LegacyCompatibilityTransport(ITransport innerTransport)
{
if (innerTransport == null)
throw new ArgumentNullException(nameof(innerTransport));
InnerTransport = innerTransport;
}
public ITransport InnerTransport { get; }
static Dictionary<string, string[]> replaceMap = new Dictionary<string, string[]>()
{
{ "--sh_wpkh", new[] {"--addr-type", "sh_wit" } },
{ "--wpkh", new[] {"--addr-type", "wit" } },
{ "--testnet", new[] {"--chain", "test" } },
};
public Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel)
{
if (arguments.Any(a => replaceMap.ContainsKey(a)))
{
List<string> newArgs = new List<string>(arguments.Length);
foreach (var a in arguments)
{
if (replaceMap.TryGetValue(a, out var replacement))
{
newArgs.AddRange(replacement);
}
else
{
newArgs.Add(a);
}
}
arguments = newArgs.ToArray();
}
return InnerTransport.SendCommandAsync(arguments, cancel);
}
}
}

View File

@ -1,5 +1,5 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>1.1.3</Version> <Version>2.0.6</Version>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -1,23 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<LangVersion>8.0</LangVersion> <LangVersion>12</LangVersion>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0-preview-20200116-01" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -23,7 +23,7 @@ namespace BTCPayServer.Vault.Tests
private ILogger _logger; private ILogger _logger;
private ILogger _HwiLogger; private ILogger _HwiLogger;
public HwiTester(ILoggerFactory loggerFactory, string hwiPath) public HwiTester(Network network, ILoggerFactory loggerFactory, string hwiPath)
{ {
if (hwiPath == null) if (hwiPath == null)
throw new ArgumentNullException(nameof(hwiPath)); throw new ArgumentNullException(nameof(hwiPath));
@ -31,20 +31,21 @@ namespace BTCPayServer.Vault.Tests
throw new ArgumentNullException(nameof(loggerFactory)); throw new ArgumentNullException(nameof(loggerFactory));
_logger = loggerFactory.CreateLogger("HwiTester"); _logger = loggerFactory.CreateLogger("HwiTester");
_HwiLogger = loggerFactory.CreateLogger("CliTransport"); _HwiLogger = loggerFactory.CreateLogger("CliTransport");
Client = new HwiClient(Network) Network = network;
Client = new HwiClient(network)
{ {
IgnoreInvalidNetwork = true, IgnoreInvalidNetwork = true,
Transport = new CliTransport(hwiPath) Transport = new LegacyCompatibilityTransport(new CliTransport(Path.GetDirectoryName(hwiPath))
{ {
Logger = _HwiLogger Logger = _HwiLogger
} })
}; };
} }
public static async Task<HwiTester> CreateAsync(ILoggerFactory loggerFactory) public static async Task<HwiTester> CreateAsync(Network network, ILoggerFactory loggerFactory)
{ {
var hwi = await HwiVersions.Latest.Current.EnsureIsDeployed(); var hwi = await HwiVersions.Latest.Current.EnsureIsDeployed();
return new HwiTester(loggerFactory, hwi); return new HwiTester(network, loggerFactory, hwi);
} }
public async Task EnsureHasDevice() public async Task EnsureHasDevice()
@ -54,7 +55,7 @@ namespace BTCPayServer.Vault.Tests
throw new InvalidOperationException("No device supported by HWI has been plugged"); throw new InvalidOperationException("No device supported by HWI has been plugged");
} }
public Network Network => NBitcoin.Network.RegTest; public Network Network { get; }
public HwiClient Client public HwiClient Client
{ {
@ -66,5 +67,21 @@ namespace BTCPayServer.Vault.Tests
get; get;
set; set;
} }
public KeyPath GetKeyPath(ScriptPubKeyType addressType)
{
var network = Network.ChainName == ChainName.Mainnet ? "0'" : "1'";
switch (addressType)
{
case ScriptPubKeyType.Legacy:
return new KeyPath($"44'/{network}/0'");
case ScriptPubKeyType.Segwit:
return new KeyPath($"84'/{network}/0'");
case ScriptPubKeyType.SegwitP2SH:
return new KeyPath($"49'/{network}/0'");
default:
throw new NotSupportedException(addressType.ToString());
}
}
} }
} }

View File

@ -5,15 +5,12 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NBitcoin; using NBitcoin;
using Xunit; using Xunit;
using Xunit.Abstractions;
using BTCPayServer.Hwi; using BTCPayServer.Hwi;
using BTCPayServer.Hwi.Transports; using BTCPayServer.Hwi.Transports;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Builder;
using BTCPayServer.Vault;
namespace BTCPayServer.Vault.Tests namespace BTCPayServer.Vault.Tests
{ {
@ -29,6 +26,26 @@ namespace BTCPayServer.Vault.Tests
ILogger Logger; ILogger Logger;
[Fact]
public void CanParseVersion()
{
var v = new[]
{
("hwi.exe 1.0.1", new Version(1,0,1)),
("hwi 1.0.1", new Version(1,0,1)),
("hwi 1.2", new Version(1,2,0)),
("pouet 2.1", new Version(2,1,0)),
("pouet 2.1rl", new Version(2,1,0)),
("pouet 2.1 rl", new Version(2,1,0)),
("long 2.1.3.4 rl", new Version(2,1,3)),
};
foreach (var o in v)
{
Assert.Equal(o.Item2, HwiParser.ParseVersion(o.Item1));
}
}
[Fact] [Fact]
public async Task CanGetVersion() public async Task CanGetVersion()
{ {
@ -76,18 +93,7 @@ namespace BTCPayServer.Vault.Tests
public async Task CanGetXPub() public async Task CanGetXPub()
{ {
var tester = await CreateTester(); var tester = await CreateTester();
await tester.Device.GetXPubAsync(new KeyPath("1'")); await tester.Device.GetXPubAsync(new KeyPath("44'/0'/0'/0/0"));
}
[Fact]
[Trait("Device", "Device")]
public async Task CanSignMessage()
{
var tester = await CreateTester();
var signature = await tester.Device.SignMessageAsync("I am satoshi", new KeyPath("44'/1'/0'/0/0"));
var xpub = await tester.Device.GetXPubAsync(new KeyPath("44'/1'/0'/0/0"));
Assert.True(xpub.GetPublicKey().VerifyMessage("I am satoshi", signature));
} }
[Fact] [Fact]
@ -95,11 +101,11 @@ namespace BTCPayServer.Vault.Tests
public async Task CanDisplayAddress() public async Task CanDisplayAddress()
{ {
var tester = await CreateTester(); var tester = await CreateTester();
await tester.Device.DisplayAddressAsync(ScriptPubKeyType.Legacy, GetKeyPath(ScriptPubKeyType.Legacy).Derive("0/1")); await tester.Device.DisplayAddressAsync(ScriptPubKeyType.Legacy, tester.GetKeyPath(ScriptPubKeyType.Legacy).Derive("0/1"));
if (tester.Network.Consensus.SupportSegwit) if (tester.Network.Consensus.SupportSegwit)
{ {
await tester.Device.DisplayAddressAsync(ScriptPubKeyType.Segwit, GetKeyPath(ScriptPubKeyType.Segwit).Derive("0/1")); await tester.Device.DisplayAddressAsync(ScriptPubKeyType.Segwit, tester.GetKeyPath(ScriptPubKeyType.Segwit).Derive("0/1"));
await tester.Device.DisplayAddressAsync(ScriptPubKeyType.SegwitP2SH, GetKeyPath(ScriptPubKeyType.SegwitP2SH).Derive("0/1")); await tester.Device.DisplayAddressAsync(ScriptPubKeyType.SegwitP2SH, tester.GetKeyPath(ScriptPubKeyType.SegwitP2SH).Derive("0/1"));
} }
} }
@ -108,7 +114,6 @@ namespace BTCPayServer.Vault.Tests
public async Task CanSign() public async Task CanSign()
{ {
var tester = await CreateTester(); var tester = await CreateTester();
// Should show we are sending 2.0 BTC three time // Should show we are sending 2.0 BTC three time
var psbt = await tester.Device.SignPSBTAsync(await CreatePSBT(tester, ScriptPubKeyType.Legacy)); var psbt = await tester.Device.SignPSBTAsync(await CreatePSBT(tester, ScriptPubKeyType.Legacy));
AssertFullySigned(tester, psbt); AssertFullySigned(tester, psbt);
@ -131,7 +136,7 @@ namespace BTCPayServer.Vault.Tests
private async Task<PSBT> CreatePSBT(HwiTester tester, ScriptPubKeyType addressType) private async Task<PSBT> CreatePSBT(HwiTester tester, ScriptPubKeyType addressType)
{ {
var accountKeyPath = new RootedKeyPath(tester.Device.Fingerprint.Value, GetKeyPath(addressType)); var accountKeyPath = new RootedKeyPath(tester.Device.Fingerprint.Value, tester.GetKeyPath(addressType));
var accountKey = await tester.Device.GetXPubAsync(accountKeyPath.KeyPath); var accountKey = await tester.Device.GetXPubAsync(accountKeyPath.KeyPath);
Logger.LogInformation($"Signing with xpub {accountKeyPath}: {accountKey}..."); Logger.LogInformation($"Signing with xpub {accountKeyPath}: {accountKey}...");
List<Transaction> knownTransactions = new List<Transaction>(); List<Transaction> knownTransactions = new List<Transaction>();
@ -148,28 +153,13 @@ namespace BTCPayServer.Vault.Tests
return psbt; return psbt;
} }
private KeyPath GetKeyPath(ScriptPubKeyType addressType)
{
switch (addressType)
{
case ScriptPubKeyType.Legacy:
return new KeyPath("44'/1'/0'");
case ScriptPubKeyType.Segwit:
return new KeyPath("84'/1'/0'");
case ScriptPubKeyType.SegwitP2SH:
return new KeyPath("49'/1'/0'");
default:
throw new NotSupportedException(addressType.ToString());
}
}
private void CreateCoin(TransactionBuilder builder, List<Transaction> knownTransactions, ScriptPubKeyType addressType, Money money, BitcoinExtPubKey xpub, string path) private void CreateCoin(TransactionBuilder builder, List<Transaction> knownTransactions, ScriptPubKeyType addressType, Money money, BitcoinExtPubKey xpub, string path)
{ {
var pubkey = xpub.Derive(new KeyPath(path)).ExtPubKey.PubKey; var pubkey = xpub.Derive(new KeyPath(path)).ExtPubKey.PubKey;
if (addressType == ScriptPubKeyType.Legacy) if (addressType == ScriptPubKeyType.Legacy)
{ {
var prevTx = xpub.Network.CreateTransaction(); var prevTx = xpub.Network.CreateTransaction();
prevTx.Inputs.Add(RandomOutpoint(), new Key().ScriptPubKey); prevTx.Inputs.Add(RandomOutpoint(), ((IDestination)new Key()).ScriptPubKey);
var txout = prevTx.Outputs.Add(money, pubkey.GetScriptPubKey(addressType)); var txout = prevTx.Outputs.Add(money, pubkey.GetScriptPubKey(addressType));
var coin = new Coin(new OutPoint(prevTx, 0), txout); var coin = new Coin(new OutPoint(prevTx, 0), txout);
builder.AddCoins(coin); builder.AddCoins(coin);
@ -199,10 +189,13 @@ namespace BTCPayServer.Vault.Tests
{ {
return new OutPoint(RandomUtils.GetUInt256(), 0); return new OutPoint(RandomUtils.GetUInt256(), 0);
} }
Task<HwiTester> CreateTester(bool needDevice = true)
async Task<HwiTester> CreateTester(bool needDevice = true)
{ {
var tester = await HwiTester.CreateAsync(LoggerFactory); return CreateTester(Network.Main, needDevice);
}
async Task<HwiTester> CreateTester(Network network, bool needDevice = true)
{
var tester = await HwiTester.CreateAsync(network, LoggerFactory);
if (needDevice) if (needDevice)
await tester.EnsureHasDevice(); await tester.EnsureHasDevice();
return tester; return tester;

View File

@ -1,19 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Xunit.Abstractions; using Xunit;
namespace BTCPayServer.Vault.Tests namespace BTCPayServer.Vault.Tests
{ {
class XUnitLoggerFactory : ILoggerFactory class XUnitLoggerFactory(ITestOutputHelper testOutput) : ILoggerFactory
{ {
public XUnitLoggerFactory(ITestOutputHelper testOutput) public ITestOutputHelper TestOutput { get; } = testOutput;
{
TestOutput = testOutput;
}
public ITestOutputHelper TestOutput { get; }
public void AddProvider(ILoggerProvider provider) public void AddProvider(ILoggerProvider provider)
{ {

View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 17
VisualStudioVersion = 16.0.29411.108 VisualStudioVersion = 17.1.32414.318
MinimumVisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Vault", "BTCPayServer.Vault\BTCPayServer.Vault.csproj", "{E54675AA-679D-440B-B82C-52FC2E119C34}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Vault", "BTCPayServer.Vault\BTCPayServer.Vault.csproj", "{E54675AA-679D-440B-B82C-52FC2E119C34}"
EndProject EndProject

View File

@ -2,8 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="BTCPayServer.Vault.App"> x:Class="BTCPayServer.Vault.App">
<Application.Styles> <Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/> <SimpleTheme />
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
<StyleInclude Source="avares://BTCPayServer.Vault/Styles.xaml" /> <StyleInclude Source="avares://BTCPayServer.Vault/Styles.xaml" />
</Application.Styles> </Application.Styles>
</Application> </Application>

View File

@ -18,7 +18,7 @@ namespace BTCPayServer.Vault
{ {
} }
public IServiceProvider ServiceProvider { get; private set; }
public IHostApplicationLifetime HostApplicationLifetime { get; private set; } public IHostApplicationLifetime HostApplicationLifetime { get; private set; }
public IHost Host { get; private set; } public IHost Host { get; private set; }
public IClassicDesktopStyleApplicationLifetime Desktop { get; private set; } public IClassicDesktopStyleApplicationLifetime Desktop { get; private set; }
@ -30,15 +30,14 @@ namespace BTCPayServer.Vault
} }
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
ServiceProvider = AvaloniaLocator.CurrentMutable.GetService<IServiceProvider>();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
Desktop = desktop; Desktop = desktop;
Desktop.MainWindow = ServiceProvider.GetRequiredService<MainWindow>(); Desktop.MainWindow = Program.CurrentServiceProvider.GetRequiredService<MainWindow>();
Desktop.Exit += Desktop_Exit; Desktop.Exit += Desktop_Exit;
Desktop.Startup += Desktop_Startup; Desktop.Startup += Desktop_Startup;
HostApplicationLifetime = ServiceProvider.GetRequiredService<IHostApplicationLifetime>(); HostApplicationLifetime = Program.CurrentServiceProvider.GetRequiredService<IHostApplicationLifetime>();
Host = ServiceProvider.GetRequiredService<IHost>(); Host = Program.CurrentServiceProvider.GetRequiredService<IHost>();
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }

View File

@ -39,7 +39,7 @@ namespace BTCPayServer.Vault
if (ReferenceEquals(platformImpl, null)) if (ReferenceEquals(platformImpl, null))
return; return;
var platformHandle = platformImpl.Handle; var platformHandle = window.TryGetPlatformHandle();
if (ReferenceEquals(platformHandle, null)) if (ReferenceEquals(platformHandle, null))
return; return;
@ -65,7 +65,7 @@ namespace BTCPayServer.Vault
if (ReferenceEquals(platformImpl, null)) if (ReferenceEquals(platformImpl, null))
return; return;
var platformHandle = platformImpl.Handle; var platformHandle = window.TryGetPlatformHandle();
if (ReferenceEquals(platformHandle, null)) if (ReferenceEquals(platformHandle, null))
return; return;
var handle = platformHandle.Handle; var handle = platformHandle.Handle;

View File

@ -3,7 +3,8 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<OutputType Condition=" '$(Configuration)' == 'Release' ">WinExe</OutputType> <OutputType Condition=" '$(Configuration)' == 'Release' ">WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<LangVersion>12</LangVersion>
<Company>The BTCPayServer Team</Company> <Company>The BTCPayServer Team</Company>
<Title>BTCPayServer Vault</Title> <Title>BTCPayServer Vault</Title>
<AssemblyTitle>$(Title)</AssemblyTitle> <AssemblyTitle>$(Title)</AssemblyTitle>
@ -16,13 +17,15 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Avalonia.Desktop" Version="11.3.14" />
<PackageReference Include="Avalonia.Themes.Simple" Version="11.3.14" />
<PackageReference Include="BTCPayServer.NTag424.PCSC" Version="1.0.22" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.203" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="0.9.2" /> <PackageReference Include="Avalonia" Version="11.3.14" />
<PackageReference Include="AvalonStudio.Shell" Version="0.9.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.1" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.1.0" /> <PackageReference Include="NicolasDorier.RateLimits" Version="1.1.0" />
</ItemGroup> </ItemGroup>
@ -55,6 +58,13 @@
<PropertyGroup Condition="'$(GithubDistrib)' == 'true'"> <PropertyGroup Condition="'$(GithubDistrib)' == 'true'">
<PublishTrimmed>true</PublishTrimmed> <PublishTrimmed>true</PublishTrimmed>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(GithubDistrib)' == 'true'">
<TrimmerRootAssembly Include="BTCPayServer.Vault" />
<TrimmerRootAssembly Include="Avalonia.Themes.Simple" />
<TrimmerRootAssembly Include="Avalonia.Base" />
<TrimmerRootAssembly Include="Avalonia.FreeDesktop" />
</ItemGroup>
</Project> </Project>

View File

@ -10,13 +10,12 @@ using Avalonia.Controls;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Threading; using Avalonia.Threading;
using AvalonStudio.Shell;
using AvalonStudio.Shell.Extensibility.Platforms;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using System.Threading; using System.Threading;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Vault namespace BTCPayServer.Vault
{ {
@ -24,8 +23,6 @@ namespace BTCPayServer.Vault
{ {
public static void AddAvalonia<TApp>(this IServiceCollection services) where TApp : Application, new() public static void AddAvalonia<TApp>(this IServiceCollection services) where TApp : Application, new()
{ {
bool useGpuLinux = true;
var result = AppBuilder.Configure<TApp>(); var result = AppBuilder.Configure<TApp>();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@ -34,36 +31,17 @@ namespace BTCPayServer.Vault
.UseWin32() .UseWin32()
.UseSkia(); .UseSkia();
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
if (DetectLLVMPipeRasterizer())
{
useGpuLinux = false;
}
result.UsePlatformDetect();
}
else else
{ {
result.UsePlatformDetect(); result.UsePlatformDetect();
} }
// TODO remove this overriding of RenderTimer when Avalonia 0.9 is released.
// fixes "Thread Leak" issue in 0.8.1 Avalonia.
var old = result.WindowingSubsystemInitializer;
result.UseWindowingSubsystem(() =>
{
old();
AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60));
});
var title = GetTitle(); var title = GetTitle();
result = result result = result
.With(new Win32PlatformOptions { AllowEglInitialization = true, UseDeferredRendering = true }) .With(new Win32PlatformOptions())
.With(new X11PlatformOptions { UseGpu = useGpuLinux, WmClass = title }) .With(new X11PlatformOptions { WmClass = title })
.With(new AvaloniaNativePlatformOptions { UseDeferredRendering = true, UseGpu = true }) .With(new AvaloniaNativePlatformOptions())
.With(new MacOSPlatformOptions { ShowInDock = true }); .With(new MacOSPlatformOptions { ShowInDock = true });
services.AddSingleton(result); services.AddSingleton(result);
services.AddSingleton<MainWindow>(); services.AddSingleton<MainWindow>();
@ -121,5 +99,56 @@ namespace BTCPayServer.Vault
return false; return false;
} }
public static string ToHex(this byte[] data)
{
return Convert.ToHexString(data, 0, data.Length).ToLowerInvariant();
}
public static byte[] HexToBytes(this string hex)
{
if (hex == null)
throw new ArgumentNullException(nameof(hex));
if (hex.Length % 2 == 1)
throw new FormatException("Invalid Hex String");
if (hex.Length < (hex.Length >> 1))
throw new ArgumentException("output should be bigger", nameof(hex));
var output = new byte[hex.Length / 2];
try
{
for (int i = 0, j = 0; i < hex.Length; i += 2, j++)
{
var a = IsDigitCore(hex[i]);
var b = IsDigitCore(hex[i + 1]);
if (a == 0xff || b == 0xff)
throw new FormatException("Invalid Hex String");
output[j] = (byte)(((uint)a << 4) | (uint)b);
}
}
catch (IndexOutOfRangeException) { throw new FormatException("Invalid Hex String"); }
return output;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static byte IsDigitCore(char c)
{
return CharToHexLookup[c];
}
static byte[] CharToHexLookup => new byte[]
{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255
};
} }
} }

View File

@ -37,8 +37,19 @@ namespace BTCPayServer.Vault.HWI
internal async Task Handle(HttpContext ctx) internal async Task Handle(HttpContext ctx)
{ {
if (ctx.Request.Path.Value == "") if (!ctx.Request.Headers.TryGetValue("Origin", out var origin))
{ {
ctx.Response.StatusCode = 400;
return;
}
var originReason = new OriginReason(origin, "hwi");
if (ctx.Request.Path.Value == "" || ctx.Request.Path.Value == "/")
{
if (!await _permissionsService.IsGranted(originReason))
{
ctx.Response.StatusCode = 401;
return;
}
if (!(await TryExtractArguments(ctx.Request, ctx.RequestAborted) is string[] args)) if (!(await TryExtractArguments(ctx.Request, ctx.RequestAborted) is string[] args))
{ {
ctx.Response.StatusCode = 400; ctx.Response.StatusCode = 400;
@ -52,23 +63,19 @@ namespace BTCPayServer.Vault.HWI
} }
else if (ctx.Request.Path.StartsWithSegments("/request-permission")) else if (ctx.Request.Path.StartsWithSegments("/request-permission"))
{ {
if (!ctx.Request.Headers.TryGetValue("Origin", out var origin))
{
ctx.Response.StatusCode = 400;
return;
}
if (!await _rateLimitService.Throttle(RateLimitZones.Prompt, ThrottleSingletonObject, ctx.RequestAborted)) if (!await _rateLimitService.Throttle(RateLimitZones.Prompt, ThrottleSingletonObject, ctx.RequestAborted))
{ {
ctx.Response.StatusCode = 429; ctx.Response.StatusCode = 429;
return; return;
} }
if (await _permissionsService.IsGranted(origin))
if (await _permissionsService.IsGranted(originReason))
{ {
ctx.Response.StatusCode = 200; ctx.Response.StatusCode = 200;
return; return;
} }
if (!await _permissionPrompt.AskPermission(origin, ctx.RequestAborted)) if (!await _permissionPrompt.AskPermission(originReason, ctx.RequestAborted))
{ {
_logger.LogInformation($"Permission to {origin} got denied"); _logger.LogInformation($"Permission to {origin} got denied");
ctx.Response.StatusCode = 401; ctx.Response.StatusCode = 401;

View File

@ -29,10 +29,10 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddSingleton<ITransport>(provider => services.AddSingleton<ITransport>(provider =>
{ {
var options = provider.GetRequiredService<IOptions<HwiServerOptions>>(); var options = provider.GetRequiredService<IOptions<HwiServerOptions>>();
return new InternalTransport(new CliTransport(options.Value.HwiDeploymentDirectory) return new InternalTransport(new LegacyCompatibilityTransport(new CliTransport(options.Value.HwiDeploymentDirectory)
{ {
Logger = provider.GetRequiredService<ILoggerFactory>().CreateLogger(LoggerNames.HwiServerCli) Logger = provider.GetRequiredService<ILoggerFactory>().CreateLogger(LoggerNames.HwiServerCli)
}); }));
}); });
services.AddSingleton<IRunningIndicator>(provider => services.AddSingleton<IRunningIndicator>(provider =>
{ {

View File

@ -8,6 +8,6 @@ namespace BTCPayServer.Vault.HWI
{ {
public interface IPermissionPrompt public interface IPermissionPrompt
{ {
Task<bool> AskPermission(string origin, CancellationToken cancellationToken); Task<bool> AskPermission(OriginReason originReason, CancellationToken cancellationToken);
} }
} }

View File

@ -9,6 +9,7 @@ namespace BTCPayServer.Vault
{ {
public const string Vault = ""; public const string Vault = "";
public const string HwiServer = "BTCPayServer.Vault.Hwi"; public const string HwiServer = "BTCPayServer.Vault.Hwi";
public const string NFCServer = "BTCPayServer.Vault.NFC";
public const string HwiServerCli = "BTCPayServer.Vault.Hwi.Cli"; public const string HwiServerCli = "BTCPayServer.Vault.Hwi.Cli";
} }
} }

View File

@ -3,7 +3,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" mc:Ignorable="d"
SizeToContent="Manual" SizeToContent="Height"
CanResize="false"
Width="622" Width="622"
Height="433" Height="433"
Icon="avares://BTCPayServer.Vault/Assets/BTCPayServerVault-256x256.png" Icon="avares://BTCPayServer.Vault/Assets/BTCPayServerVault-256x256.png"
@ -17,15 +18,15 @@
<DockPanel> <DockPanel>
<TextBlock DockPanel.Dock="Left" FontSize="18">The vault is now ready to be used by web applications</TextBlock> <TextBlock DockPanel.Dock="Left" FontSize="18">The vault is now ready to be used by web applications</TextBlock>
<StackPanel DockPanel.Dock="Right" IsVisible="{Binding IsLoading}" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Horizontal"> <StackPanel DockPanel.Dock="Right" IsVisible="{Binding IsLoading}" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Horizontal">
<DrawingPresenter Drawing="{DynamicResource spinner}"></DrawingPresenter> <Image Source="{DynamicResource spinner}"/>
<TextBlock VerticalAlignment="Center" Margin="5,0,0,0" Text="{Binding CurrentOperation}"></TextBlock> <TextBlock VerticalAlignment="Center" Margin="5,0,0,0" Text="{Binding CurrentOperation}"></TextBlock>
</StackPanel> </StackPanel>
</DockPanel> </DockPanel>
<StackPanel IsVisible="{Binding IsVisible}" Margin="0,15,0,0"> <StackPanel IsVisible="{Binding IsVisible}" Margin="0,15,0,0">
<Separator HorizontalAlignment="Left" Height="3" Background="{DynamicResource btcpay-color-primary}" Width="128" Margin="0,0,0,15"></Separator> <Separator HorizontalAlignment="Left" Height="3" Background="{DynamicResource btcpay-color-primary}" Width="128" Margin="0,0,0,15"></Separator>
<DockPanel> <DockPanel>
<DrawingPresenter DockPanel.Dock="Left" Height="128" Width="128" VerticalAlignment="Center" HorizontalAlignment="Center" Drawing="{DynamicResource warning}" /> <Image DockPanel.Dock="Left" Height="128" Width="128" VerticalAlignment="Center" HorizontalAlignment="Center" Source="{DynamicResource warning}" />
<StackPanel Margin="10,0,0,0" VerticalAlignment="Center"> <StackPanel IsVisible="{Binding HWIVisible}" Margin="10,0,0,0" VerticalAlignment="Center">
<TextBlock>A website is requesting access to your hardware wallets.</TextBlock> <TextBlock>A website is requesting access to your hardware wallets.</TextBlock>
<TextBlock>If you accept, the website will be able to:</TextBlock> <TextBlock>If you accept, the website will be able to:</TextBlock>
<TextBlock Text=" "></TextBlock> <TextBlock Text=" "></TextBlock>
@ -64,6 +65,45 @@
</Button> </Button>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel IsVisible="{Binding NFCVisible}" Margin="10,0,0,0" VerticalAlignment="Center">
<TextBlock>A website is requesting access to your Smart card readers.</TextBlock>
<TextBlock>If you accept, the website will be able to:</TextBlock>
<TextBlock Text=" "></TextBlock>
<TextBlock>• Use your smart card reader to read NFC cards</TextBlock>
<TextBlock Text=" "></TextBlock>
<TextBlock>The permission will be in effect until you restart BTCPayServer Vault.</TextBlock>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Do you want to grant access to "></TextBlock>
<TextBlock FontWeight="Bold" Text="{Binding Origin}"></TextBlock>
<TextBlock>?</TextBlock>
</StackPanel>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal" HorizontalAlignment="Left">
<Button Width="100" Margin="0,0,5,0" Content="Accept" Command="{Binding Accept}">
<Button.Styles>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="#329f80"></Setter>
</Style>
<Style Selector="Button">
<Setter Property="Background" Value="#1e7e34"></Setter>
<Setter Property="Foreground" Value="White"></Setter>
</Style>
</Button.Styles>
</Button>
<Button Width="100" Content="Reject" Command="{Binding Reject}">
<Button.Styles>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="#dc3545"></Setter>
</Style>
<Style Selector="Button">
<Setter Property="Background" Value="#bd2130"></Setter>
<Setter Property="Foreground" Value="White"></Setter>
</Style>
</Button.Styles>
</Button>
</StackPanel>
</StackPanel>
</DockPanel> </DockPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

View File

@ -16,26 +16,6 @@ namespace BTCPayServer.Vault
{ {
public class MainWindow : Window public class MainWindow : Window
{ {
static Size NormalSize = new Size(622, 220);
static Size ExpandedSize = new Size(622, 433);
/// <summary>
/// Workaround https://github.com/AvaloniaUI/Avalonia/issues/3290 and https://github.com/AvaloniaUI/Avalonia/issues/3291
/// Because our app can only have two size we just resize based on the state of IsVisible of the MainViewModel
/// </summary>
void ResizeHack()
{
Size newSize = MainViewModel.IsVisible ? ExpandedSize : NormalSize;
if (newSize != this.ClientSize)
{
this.ClientSize = newSize;
// On Mac, resizing down will make the windows jump down, so we need to set it back up
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && newSize == NormalSize)
{
this.Position = this.Position.WithY(this.Position.Y - (int)(ExpandedSize.Height - NormalSize.Height));
}
}
}
public MainWindow() public MainWindow()
{ {
@ -43,12 +23,11 @@ namespace BTCPayServer.Vault
Title = Extensions.GetTitle(); Title = Extensions.GetTitle();
} }
DispatcherTimer _ResizeHackTimer; private DispatcherTimer _BlinkTimer;
private void InitializeComponent() private void InitializeComponent()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
Context = AvaloniaSynchronizationContext.Current as AvaloniaSynchronizationContext; if (Program.CurrentServiceProvider is { } serviceProvider)
if (AvaloniaLocator.CurrentMutable?.GetService<IServiceProvider>() is IServiceProvider serviceProvider)
{ {
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
Indicator = ServiceProvider.GetRequiredService<IRunningIndicator>(); Indicator = ServiceProvider.GetRequiredService<IRunningIndicator>();
@ -56,14 +35,12 @@ namespace BTCPayServer.Vault
Indicator.StoppedRunning += OnStoppedRunning; Indicator.StoppedRunning += OnStoppedRunning;
DataContext = ServiceProvider.GetRequiredService<MainWindowViewModel>(); DataContext = ServiceProvider.GetRequiredService<MainWindowViewModel>();
MainViewModel.PropertyChanged += MainViewModel_PropertyChanged; MainViewModel.PropertyChanged += MainViewModel_PropertyChanged;
_ResizeHackTimer = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Normal, (_, __) => _BlinkTimer = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Normal, (_, __) =>
{ {
ResizeHack();
if (MainViewModel.IsVisible && this.WindowState == WindowState.Minimized) if (MainViewModel.IsVisible && this.WindowState == WindowState.Minimized)
this.Blink(); this.Blink();
}); });
_ResizeHackTimer.Start(); _BlinkTimer.Start();
this.ResizeHack();
} }
} }
private void MainViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) private void MainViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
@ -72,13 +49,12 @@ namespace BTCPayServer.Vault
{ {
Context.Post(_ => Context.Post(_ =>
{ {
this.ResizeHack();
this.ActivateHack(); this.ActivateHack();
}, null); }, null);
} }
} }
protected override void OnClosing(CancelEventArgs e) protected override void OnClosing(WindowClosingEventArgs e)
{ {
base.OnClosing(e); base.OnClosing(e);
if (Indicator != null) if (Indicator != null)
@ -86,7 +62,7 @@ namespace BTCPayServer.Vault
Indicator.Running -= OnRunning; Indicator.Running -= OnRunning;
Indicator.StoppedRunning -= OnStoppedRunning; Indicator.StoppedRunning -= OnStoppedRunning;
MainViewModel.PropertyChanged -= MainViewModel_PropertyChanged; MainViewModel.PropertyChanged -= MainViewModel_PropertyChanged;
_ResizeHackTimer.Stop(); _BlinkTimer.Stop();
} }
} }
@ -117,14 +93,14 @@ namespace BTCPayServer.Vault
public IServiceProvider ServiceProvider { get; private set; } public IServiceProvider ServiceProvider { get; private set; }
public IRunningIndicator Indicator { get; private set; } public IRunningIndicator Indicator { get; private set; }
AvaloniaSynchronizationContext Context; AvaloniaSynchronizationContext Context = new AvaloniaSynchronizationContext();
internal async Task<bool> Authorize(string origin) internal async Task<bool> Authorize(OriginReason originReason)
{ {
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Context.Post((state) => Context.Post((state) =>
{ {
MainViewModel.Authorize(origin, tcs); MainViewModel.Authorize(originReason, tcs);
}, null); }, null);
return await tcs.Task; return await tcs.Task;
} }

View File

@ -36,16 +36,14 @@ namespace BTCPayServer.Vault
{ {
this.Accept = new LambdaCommand(() => this.Accept = new LambdaCommand(() =>
{ {
this.IsVisible = false; this.AuthorizedOrigins.Add(OriginReason);
this.AuthorizedOrigins.Add(this.Origin); OriginReason = null;
this.Origin = null;
this.taskCompletionSource.TrySetResult(true); this.taskCompletionSource.TrySetResult(true);
this.taskCompletionSource = null; this.taskCompletionSource = null;
}); });
this.Reject = new LambdaCommand(() => this.Reject = new LambdaCommand(() =>
{ {
this.IsVisible = false; this.OriginReason = null;
this.Origin = null;
this.taskCompletionSource.TrySetResult(false); this.taskCompletionSource.TrySetResult(false);
this.taskCompletionSource = null; this.taskCompletionSource = null;
}); });
@ -71,7 +69,78 @@ namespace BTCPayServer.Vault
} }
} }
public List<string> AuthorizedOrigins { get; set; } = new List<string>();
private bool _HWIVisible;
public bool HWIVisible
{
get
{
return _HWIVisible;
}
set
{
if (value != _HWIVisible)
{
_HWIVisible = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("HWIVisible"));
}
}
}
private bool _NFCVisible;
public bool NFCVisible
{
get
{
return _NFCVisible;
}
set
{
if (value != _NFCVisible)
{
_NFCVisible = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("NFCVisible"));
}
}
}
public List<OriginReason> AuthorizedOrigins { get; set; } = new List<OriginReason>();
OriginReason _OriginReason;
public OriginReason OriginReason
{
get
{
return _OriginReason;
}
set
{
if (_OriginReason != value)
{
_OriginReason = value;
if (value is null)
{
Origin = null;
IsVisible = false;
HWIVisible = false;
NFCVisible = false;
}
else
{
Origin = value.Origin;
IsVisible = true;
HWIVisible = value.Reason == "hwi";
NFCVisible = value.Reason == "nfc";
}
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("OriginReason"));
}
}
}
private string _Origin; private string _Origin;
public string Origin public string Origin
@ -91,6 +160,8 @@ namespace BTCPayServer.Vault
} }
} }
public ICommand Accept { get; } public ICommand Accept { get; }
public ICommand Reject { get; } public ICommand Reject { get; }
@ -125,25 +196,25 @@ namespace BTCPayServer.Vault
} }
TaskCompletionSource<bool> taskCompletionSource; TaskCompletionSource<bool> taskCompletionSource;
internal void Authorize(string origin, TaskCompletionSource<bool> tcs) internal void Authorize(OriginReason originReason, TaskCompletionSource<bool> tcs)
{ {
if (AuthorizedOrigins.Contains(origin)) if (AuthorizedOrigins.Contains(originReason))
{ {
tcs.TrySetResult(true); tcs.TrySetResult(true);
return; return;
} }
if (taskCompletionSource != null) if (taskCompletionSource != null)
{ {
if (Origin != origin) if (_OriginReason != originReason)
taskCompletionSource.TrySetResult(false); taskCompletionSource.TrySetResult(false);
else else
taskCompletionSource.Task.ContinueWith(result => taskCompletionSource.TrySetResult(result.Result)); taskCompletionSource.Task.ContinueWith(result => taskCompletionSource?.TrySetResult(result.Result));
return; return;
} }
else else
{ {
IsVisible = true; OriginReason = originReason;
Origin = origin;
taskCompletionSource = tcs; taskCompletionSource = tcs;
} }
} }

View File

@ -0,0 +1,159 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Logging;
using BTCPayServer.NTag424;
using BTCPayServer.NTag424.PCSC;
using BTCPayServer.Vault.HWI;
using BTCPayServer.Vault.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using NicolasDorier.RateLimits;
using PCSC;
namespace BTCPayServer.Vault.NFC
{
public class NFCServer
{
static object ThrottleSingletonObject = new object();
private readonly RateLimitService _rateLimitService;
private readonly PermissionsService _permissionsService;
private readonly ILogger _logger;
private readonly IPermissionPrompt _permissionPrompt;
public NFCServer(IPermissionPrompt permissionPrompt,
RateLimitService rateLimitService,
PermissionsService permissionsService,
ILoggerFactory loggerFactory)
{
_permissionPrompt = permissionPrompt;
_rateLimitService = rateLimitService;
_permissionsService = permissionsService;
_logger = loggerFactory.CreateLogger(LoggerNames.NFCServer);
}
PCSCContext? PCSCContext;
IAPDUTransport? ApduTransport;
internal async Task Handle(HttpContext ctx)
{
if (!ctx.Request.Headers.TryGetValue("Origin", out var origin))
{
ctx.Response.StatusCode = 400;
return;
}
var originReason = new OriginReason(origin, "nfc");
if (ctx.Request.Path.Value == "" || ctx.Request.Path.Value == "/")
{
if (!await _permissionsService.IsGranted(originReason))
{
ctx.Response.StatusCode = 401;
return;
}
var transport = ApduTransport;
if (transport is null)
{
ctx.Response.StatusCode = 409;
return;
}
var apdu = await TryExtractAPDU(ctx.Request, ctx.RequestAborted);
if (apdu is null)
{
ctx.Response.StatusCode = 400;
return;
}
var resp = await transport.SendAPDU(apdu, ctx.RequestAborted);
JsonObject response = new JsonObject()
{
["data"] = resp.Data.ToHex(),
["status"] = resp.sw1sw2
};
ctx.Response.StatusCode = 200;
ctx.Response.Headers["Content-Type"] = "application/json";
await ctx.Response.WriteAsync(response.ToJsonString(), ctx.RequestAborted);
}
if (ctx.Request.Path.StartsWithSegments("/wait-for-card"))
{
if (!await _permissionsService.IsGranted(originReason))
{
ctx.Response.StatusCode = 401;
return;
}
PCSCContext?.Dispose();
PCSCContext = await PCSCContext.WaitForCard(ctx.RequestAborted);
ApduTransport = new PCSCAPDUTransport(PCSCContext.CardReader);
_logger.LogInformation($"NFC card detected");
ctx.Response.StatusCode = 200;
return;
}
if (ctx.Request.Path.StartsWithSegments("/wait-for-disconnected"))
{
if (!await _permissionsService.IsGranted(originReason))
{
ctx.Response.StatusCode = 401;
return;
}
if (PCSCContext is null)
{
ctx.Response.StatusCode = 409;
return;
}
await PCSCContext.WaitForDisconnected(ctx.RequestAborted);
PCSCContext.Dispose();
PCSCContext = null;
ApduTransport = null;
_logger.LogInformation($"NFC card disconnected");
ctx.Response.StatusCode = 200;
return;
}
else if (ctx.Request.Path.StartsWithSegments("/request-permission"))
{
if (!await _rateLimitService.Throttle(RateLimitZones.Prompt, ThrottleSingletonObject, ctx.RequestAborted))
{
ctx.Response.StatusCode = 429;
return;
}
if (await _permissionsService.IsGranted(originReason))
{
ctx.Response.StatusCode = 200;
return;
}
if (!await _permissionPrompt.AskPermission(originReason, ctx.RequestAborted))
{
_logger.LogInformation($"Permission to {origin} got denied");
ctx.Response.StatusCode = 401;
return;
}
_logger.LogInformation($"Permission to {origin} got granted");
ctx.Response.StatusCode = 200;
return;
}
}
private static async Task<byte[]?> TryExtractAPDU(HttpRequest request, CancellationToken cancellationToken)
{
var document = await JsonDocument.ParseAsync(request.Body, cancellationToken: cancellationToken);
if (document.RootElement.TryGetProperty("apdu", out var apdu) &&
apdu.ValueKind == JsonValueKind.String)
{
try
{
return apdu.GetString().HexToBytes();
}
catch
{
return null;
}
}
return null;
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Vault.HWI;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Vault.NFC
{
public static class NFCServerExtensions
{
public static IServiceCollection AddNFCServer(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.AddCors();
services.AddSingleton<NFCServer>();
return services;
}
public static IApplicationBuilder UseNFCServer(this IApplicationBuilder applicationBuilder)
{
ArgumentNullException.ThrowIfNull(applicationBuilder);
applicationBuilder.Map(new PathString("/nfc-bridge/v1"), app =>
{
app.UseCors(policy => policy.AllowAnyOrigin().WithMethods("POST"));
app.Run(async ctx =>
{
await ctx.RequestServices.GetRequiredService<NFCServer>().Handle(ctx);
});
});
return applicationBuilder;
}
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Vault
{
public record OriginReason(string Origin, string Reason);
}

View File

@ -9,25 +9,29 @@ using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using BTCPayServer.Vault.Services;
namespace BTCPayServer.Vault namespace BTCPayServer.Vault
{ {
public class PermissionPrompt : IPermissionPrompt public class PermissionPrompt : IPermissionPrompt
{ {
private readonly LinkGenerator _linkGenerator; private readonly LinkGenerator _linkGenerator;
private readonly PermissionsService _permissionsService;
private readonly MainWindow _mainWindow; private readonly MainWindow _mainWindow;
HttpContext httpContext;
public PermissionPrompt(LinkGenerator linkGenerator, public PermissionPrompt(LinkGenerator linkGenerator,
IHttpContextAccessor httpContextAccessor, PermissionsService permissionsService,
MainWindow mainWindow) MainWindow mainWindow)
{ {
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_permissionsService = permissionsService;
_mainWindow = mainWindow; _mainWindow = mainWindow;
httpContext = httpContextAccessor.HttpContext;
} }
public async Task<bool> AskPermission(string origin, CancellationToken cancellationToken) public async Task<bool> AskPermission(OriginReason originReason, CancellationToken cancellationToken)
{ {
return await _mainWindow.Authorize(origin); var result = await _mainWindow.Authorize(originReason);
if (result)
await _permissionsService.Grant(originReason);
return result;
} }
} }
} }

View File

@ -16,7 +16,6 @@ using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using System.IO; using System.IO;
using Avalonia; using Avalonia;
using Avalonia.Logging.Serilog;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
@ -48,12 +47,17 @@ namespace BTCPayServer.Vault
#endif #endif
}) })
.Build(); .Build();
CurrentServiceProvider = host.Services;
host.Services.GetRequiredService<AppBuilder>() host.Services.GetRequiredService<AppBuilder>()
.With(host.Services) .With(host.Services)
.With(host) .With(host)
.StartWithClassicDesktopLifetime(args); .StartWithClassicDesktopLifetime(args);
} }
public static IServiceProvider CurrentServiceProvider { get; private set; }
private static bool TestPortFree() private static bool TestPortFree()
{ {
TcpListener listener = new TcpListener(IPAddress.Loopback, HttpTransport.LocalHwiDefaultPort); TcpListener listener = new TcpListener(IPAddress.Loopback, HttpTransport.LocalHwiDefaultPort);
@ -73,6 +77,6 @@ namespace BTCPayServer.Vault
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>() => AppBuilder.Configure<App>()
.UsePlatformDetect() .UsePlatformDetect()
.LogToDebug(); .LogToTrace();
} }
} }

View File

@ -3,20 +3,21 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Vault.Services namespace BTCPayServer.Vault.Services
{ {
public class PermissionsService public class PermissionsService
{ {
ConcurrentDictionary<string, GrantedPermission> _permissions = new ConcurrentDictionary<string, GrantedPermission>(); ConcurrentDictionary<OriginReason, GrantedPermission> _permissions = new ConcurrentDictionary<OriginReason, GrantedPermission>();
public Task Grant(string origin) public Task Grant(OriginReason originReason)
{ {
_permissions.TryAdd(origin, new GrantedPermission(origin)); _permissions.TryAdd(originReason, new GrantedPermission(originReason));
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task UpdateAccessed(string origin) public Task UpdateAccessed(OriginReason originReason)
{ {
if (_permissions.TryGetValue(origin, out var permission)) if (_permissions.TryGetValue(originReason, out var permission))
permission.LastAccessed = DateTimeOffset.UtcNow; permission.LastAccessed = DateTimeOffset.UtcNow;
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -26,28 +27,28 @@ namespace BTCPayServer.Vault.Services
return Task.FromResult(_permissions.Values); return Task.FromResult(_permissions.Values);
} }
public Task Revoke(string origin) public Task Revoke(OriginReason originReason)
{ {
_permissions.TryRemove(origin, out _); _permissions.TryRemove(originReason, out _);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<bool> IsGranted(string origin) public Task<bool> IsGranted(OriginReason originReason)
{ {
return Task.FromResult(_permissions.TryGetValue(origin, out _)); return Task.FromResult(_permissions.TryGetValue(originReason, out _));
} }
} }
public class GrantedPermission public class GrantedPermission
{ {
public GrantedPermission(string origin) public GrantedPermission(OriginReason originReason)
{ {
Origin = origin; OriginReason = originReason;
Created = DateTimeOffset.UtcNow; Created = DateTimeOffset.UtcNow;
} }
public DateTimeOffset Created { get; set; } public DateTimeOffset Created { get; set; }
public DateTimeOffset? LastAccessed { get; set; } public DateTimeOffset? LastAccessed { get; set; }
public string Origin { get; set; } public OriginReason OriginReason { get; set; }
} }
} }

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using BTCPayServer.Vault.HWI; using BTCPayServer.Vault.HWI;
using BTCPayServer.Vault.NFC;
using BTCPayServer.Vault.Services; using BTCPayServer.Vault.Services;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -16,8 +17,9 @@ namespace BTCPayServer.Vault
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddHwiServer(); services.AddHwiServer();
services.AddNFCServer();
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddScoped<HWI.IPermissionPrompt, PermissionPrompt>(); services.AddSingleton<HWI.IPermissionPrompt, PermissionPrompt>();
services.Configure<HwiServerOptions>(opt => opt.HwiDeploymentDirectory = Path.GetDirectoryName(typeof(Program).Assembly.Location)); services.Configure<HwiServerOptions>(opt => opt.HwiDeploymentDirectory = Path.GetDirectoryName(typeof(Program).Assembly.Location));
services.AddSingleton<PermissionsService>(); services.AddSingleton<PermissionsService>();
services.AddRateLimits(); services.AddRateLimits();
@ -30,6 +32,7 @@ namespace BTCPayServer.Vault
rateLimitService.SetZone($"zone={RateLimitZones.Prompt} rate=4r/m burst=3"); rateLimitService.SetZone($"zone={RateLimitZones.Prompt} rate=4r/m burst=3");
app.UseStaticFiles(); app.UseStaticFiles();
app.UseHwiServer(); app.UseHwiServer();
app.UseNFCServer();
app.UseRouting(); app.UseRouting();
app.UseEndpoints(e => app.UseEndpoints(e =>
{ {

View File

@ -3,16 +3,16 @@
<Style> <Style>
<Style.Resources> <Style.Resources>
<SolidColorBrush x:Key="btcpay-color-primary">#329f80</SolidColorBrush> <SolidColorBrush x:Key="btcpay-color-primary">#329f80</SolidColorBrush>
<DrawingGroup x:Key="warning"> <DrawingImage x:Key="warning">
<DrawingGroup.Children> <DrawingGroup>
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" /> <GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M7.7246,15.9219C1.6086,14.1719,0.6986,7.3389,1.0726,3.9709L1.1316,3.4369 7.9996,-9.99999999997669E-05 14.8686,3.4369 14.9276,3.9709C15.3016,7.3389,14.3916,14.1719,8.2756,15.9219L7.9996,15.9999z" /> <GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M7.7246,15.9219C1.6086,14.1719,0.6986,7.3389,1.0726,3.9709L1.1316,3.4369 7.9996,-9.99999999997669E-05 14.8686,3.4369 14.9276,3.9709C15.3016,7.3389,14.3916,14.1719,8.2756,15.9219L7.9996,15.9999z" />
<GeometryDrawing Brush="#FFFFCC00" Geometry="F1M9,9L7,9 7,4 9,4z M9,12L7,12 7,10 9,10z M13.951,4L8,1 2.049,4C2.049,4 1.058,13 8,15 14.942,13 13.951,4 13.951,4" /> <GeometryDrawing Brush="#FFFFCC00" Geometry="F1M9,9L7,9 7,4 9,4z M9,12L7,12 7,10 9,10z M13.951,4L8,1 2.049,4C2.049,4 1.058,13 8,15 14.942,13 13.951,4 13.951,4" />
<GeometryDrawing Brush="#FF000000" Geometry="F1M7,12L9,12 9,10 7,10z M7,4L9,4 9,9 7,9z" /> <GeometryDrawing Brush="#FF000000" Geometry="F1M7,12L9,12 9,10 7,10z M7,4L9,4 9,9 7,9z" />
</DrawingGroup.Children> </DrawingGroup>
</DrawingGroup> </DrawingImage>
<DrawingGroup x:Key="spinner"> <DrawingImage x:Key="spinner">
<DrawingGroup.Children> <DrawingGroup>
<GeometryDrawing Brush="#329f80"> <GeometryDrawing Brush="#329f80">
<GeometryDrawing.Geometry> <GeometryDrawing.Geometry>
<EllipseGeometry Rect="0 0 20 20"></EllipseGeometry> <EllipseGeometry Rect="0 0 20 20"></EllipseGeometry>
@ -23,8 +23,8 @@
<EllipseGeometry Rect="2.5 2.5 15 15"></EllipseGeometry> <EllipseGeometry Rect="2.5 2.5 15 15"></EllipseGeometry>
</GeometryDrawing.Geometry> </GeometryDrawing.Geometry>
</GeometryDrawing> </GeometryDrawing>
</DrawingGroup.Children> </DrawingGroup>
</DrawingGroup> </DrawingImage>
</Style.Resources> </Style.Resources>
</Style> </Style>
<Style Selector="TextBlock"> <Style Selector="TextBlock">

View File

@ -1,5 +1,5 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>1.0.6</Version> <Version>3.0.0</Version>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

9
Build/travis/applesign.md → Build/CI/applesign.md Normal file → Executable file
View File

@ -5,7 +5,7 @@ This document how to setup your travis environment so that it can properly sign
At the end of this process, you will have configured `APPLE_DEV_ID_CERT` and `APPLE_DEV_ID_CERT_PASSWORD` to the correct value for travis to sign your MAC application. At the end of this process, you will have configured `APPLE_DEV_ID_CERT` and `APPLE_DEV_ID_CERT_PASSWORD` to the correct value for travis to sign your MAC application.
You will also need to set `APPLE_ID` and `APPLE_ID_PASSWORD`, using [app-specific password](https://support.apple.com/en-us/HT204397). You will also need to set `APPLE_ID`, `APPLE_ID_PASSWORD` and `APPLE_TEAM_ID`, using [app-specific password](https://support.apple.com/en-us/HT204397).
## How to ## How to
If you are on linux and try to sign with an apple certificate you need to do the following steps: If you are on linux and try to sign with an apple certificate you need to do the following steps:
@ -28,6 +28,7 @@ Let's create it:
```bash ```bash
rsa_key_file="temp.key" rsa_key_file="temp.key"
csr_file_name="request.csr" csr_file_name="request.csr"
openssl genrsa -out "$rsa_key_file" 2048
openssl req -new -key "$rsa_key_file" -out "$csr_file_name" -subj "/emailAddress=$email, CN=$common_name, C=$country_code" openssl req -new -key "$rsa_key_file" -out "$csr_file_name" -subj "/emailAddress=$email, CN=$common_name, C=$country_code"
``` ```
This will create a request file as `request.csr` in your current folder. This will create a request file as `request.csr` in your current folder.
@ -43,7 +44,7 @@ cer_file="developerID_application.cer"
pem_file="developerID_application.pem" pem_file="developerID_application.pem"
cert_output_file="developerID_application.p12" cert_output_file="developerID_application.p12"
openssl x509 -in "$cer_file" -inform DER -out "$pem_file" -outform PEM openssl x509 -in "$cer_file" -inform DER -out "$pem_file" -outform PEM
openssl pkcs12 -export -inkey "$rsa_key_file" -in "$pem_file" -out "$cert_output_file" openssl pkcs12 -export -inkey "$rsa_key_file" -in "$pem_file" -legacy -out "$cert_output_file"
``` ```
Now enter a password, don't pick an empty one as the rest would fail. Now enter a password, don't pick an empty one as the rest would fail.
@ -60,8 +61,4 @@ For travis to sign BTCPayServer.Vault dmg file, you need to convert the certific
```bash ```bash
cat $cert_output_file | base64 -w0 cat $cert_output_file | base64 -w0
``` ```
Then setup your travis environment variable `APPLE_DEV_ID_CERT` to this value **SURROUNDED BY DOUBLE QUOTE ("")**.
Additionally setup the travis environment variable `APPLE_DEV_ID_CERT_PASSWORD`, **SURROUNDED BY DOUBLE QUOTE ("")**.
Did I say that you need to **SURROUND BY DOUBLE QUOTE ("")** any value that you enter in travis environment variable?

View File

@ -42,7 +42,7 @@ echo "Starting apple signing..."
version="$(cat BTCPayServer.Vault/Version.csproj | sed -n 's/.*<Version>\(.*\)<\/Version>.*/\1/p')" version="$(cat BTCPayServer.Vault/Version.csproj | sed -n 's/.*<Version>\(.*\)<\/Version>.*/\1/p')"
title="$(cat BTCPayServer.Vault/BTCPayServer.Vault.csproj | sed -n 's/.*<Title>\(.*\)<\/Title>.*/\1/p')" title="$(cat BTCPayServer.Vault/BTCPayServer.Vault.csproj | sed -n 's/.*<Title>\(.*\)<\/Title>.*/\1/p')"
AZURE_ACCOUNT_NAME="$(echo "$AZURE_STORAGE_CONNECTION_STRING" | cut -d'=' -f3 | cut -d';' -f1)" AZURE_ACCOUNT_NAME="$(echo "$AZURE_STORAGE_CONNECTION_STRING" | cut -d'=' -f3 | cut -d';' -f1)"
DIRECTORY_NAME="dist-$TRAVIS_BUILD_ID" DIRECTORY_NAME="dist-$GITHUB_RUN_ID"
RUNTIME="osx-x64" RUNTIME="osx-x64"
tar_file="BTCPayServerVault-${RUNTIME}-$version.tar.gz" tar_file="BTCPayServerVault-${RUNTIME}-$version.tar.gz"
@ -94,35 +94,12 @@ echo "DMG signed"
echo "Notarize $dmg_file with bundle id $bundle_id" echo "Notarize $dmg_file with bundle id $bundle_id"
sudo xcrun altool --notarize-app -t osx -f "$dmg_file" --primary-bundle-id "$bundle_id" -u "$APPLE_ID" -p "$APPLE_ID_PASSWORD" --output-format xml | tee notarize_result sudo xcrun notarytool submit --apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_TEAM_ID" --wait "$dmg_file"
request_id="$(cat notarize_result | grep -A1 "RequestUUID" | sed -n 's/\s*<string>\([^<]*\)<\/string>/\1/p' | xargs)"
echo "Notarization in progress, request id: $request_id"
echo "Waiting for approval..."
while true; do
echo -n "."
sleep 10 # We need to wait 10 sec, even for the first loop because Apple might still not have their own data...
set +e
sudo xcrun altool --notarization-info "$request_id" -u "$APPLE_ID" -p "$APPLE_ID_PASSWORD" > notarization_progress
set -e
if grep -q "Status: success" notarization_progress; then
echo ""
cat notarization_progress
echo "Notarization succeed"
break
elif grep -q "Status: in progress" notarization_progress; then
continue
elif grep -q "Could not find the RequestUUID" notarization_progress; then
continue
else
cat notarization_progress
echo "Notarization failed"
exit 1
fi
done
sudo xcrun stapler staple "$dmg_file" sudo xcrun stapler staple "$dmg_file"
echo "Installing az..." echo "Installing az..."
export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1
brew update brew update
brew install azure-cli || true brew install azure-cli || true
BLOB_NAME="$DIRECTORY_NAME/$dmg_file" BLOB_NAME="$DIRECTORY_NAME/$dmg_file"

45
Build/CI/build.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
set -e
DOCKER_IMAGE_NAME="vault-$RID"
DOCKER_BUILD_ARGS=""
if [[ "$PGP_KEY" ]]; then
DOCKER_BUILD_ARGS="--build-arg "PGP_KEY=$PGP_KEY""
fi
if [[ "$WINDOWS_CERT" ]]; then
DOCKER_BUILD_ARGS="$DOCKER_BUILD_ARGS --build-arg "WINDOWS_CERT=$WINDOWS_CERT" --build-arg "WINDOWS_CERT_PASSWORD=$WINDOWS_CERT_PASSWORD""
fi
docker build -t "$DOCKER_IMAGE_NAME" $DOCKER_BUILD_ARGS -f "Build/$RID/Dockerfile" .
docker run --rm -v "$(pwd)/dist:/opt/dist" "$DOCKER_IMAGE_NAME"
if [[ "$GITHUB_REF" ]]; then
# GITHUB_REF= refs/tags/Vault/v1.0.6-test
GITHUB_REF_NAME="$(echo $GITHUB_REF | cut -d'/' -f4 | cut -d'-' -f1)"
GITHUB_REF_NAME="Vault/$GITHUB_REF_NAME"
# GITHUB_REF_NAME= Vault/v1.0.6
ci_version="$(echo "$GITHUB_REF_NAME" | cut -d'/' -f2)"
if [[ "$ci_version" ]]; then
csproj_version="v$(cat BTCPayServer.Vault/Version.csproj | sed -n 's/.*<Version>\(.*\)<\/Version>.*/\1/p')"
if [[ "$ci_version" != "$csproj_version" ]]; then
echo "The tagged version on travis ($ci_version) is different from the csproj ($csproj_version)"
exit 1
fi
fi
fi
if ! [[ "$AZURE_STORAGE_CONNECTION_STRING" ]] || ! [[ "$AZURE_STORAGE_CONTAINER" ]]; then
echo "Skipped upload to Azure (AZURE_STORAGE_CONNECTION_STRING or AZURE_STORAGE_CONTAINER not set)"
exit 0
fi
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
az storage container create --name "$AZURE_STORAGE_CONTAINER" --public-access "container"
for file in dist/*; do
BLOB_NAME="dist-$GITHUB_RUN_ID/$(basename -- $file)"
echo "Uploading $BLOB_NAME"
az storage blob upload -f "$file" -c "$AZURE_STORAGE_CONTAINER" -n "$BLOB_NAME"
url="$(az storage blob url --container-name "$AZURE_STORAGE_CONTAINER" --name "$BLOB_NAME" --protocol "https")"
echo "Uploaded file to $url"
done

View File

@ -6,38 +6,43 @@ if ! [[ "$AZURE_STORAGE_CONNECTION_STRING" ]] || ! [[ "$AZURE_STORAGE_CONTAINER"
exit 0 exit 0
fi fi
if ! [[ "$GITHUB_TOKEN" ]]; then if ! [[ "$GITHUB_REF" ]]; then
echo "Skipping github release (GITHUB_TOKEN is not set)" echo "Skipping github release (GITHUB_REF is not set)"
exit 0 exit 0
fi fi
if ! [[ "$TRAVIS_TAG" ]]; then # GITHUB_REF= refs/tags/Vault/v1.0.6-test
echo "Skipping github release (TRAVIS_TAG is not set)" GITHUB_REF_NAME="$(echo $GITHUB_REF | cut -d'/' -f4)"
exit 0 GITHUB_REF_NAME="Vault/$GITHUB_REF_NAME"
# GITHUB_REF_NAME= Vault/v1.0.6-test
draft=false
prerelease=false
if [[ "$GITHUB_REF_NAME" == *"-test" ]]; then
draft=true
prerelease=true
fi fi
AZURE_ACCOUNT_NAME="$(echo "$AZURE_STORAGE_CONNECTION_STRING" | cut -d'=' -f3 | cut -d';' -f1)" AZURE_ACCOUNT_NAME="$(echo "$AZURE_STORAGE_CONNECTION_STRING" | cut -d'=' -f3 | cut -d';' -f1)"
DIRECTORY_NAME="dist-$TRAVIS_BUILD_ID" DIRECTORY_NAME="dist-$GITHUB_RUN_ID"
wget -O azcopy.tar.gz https://aka.ms/downloadazcopy-v10-linux wget -O azcopy.tar.gz https://aka.ms/downloadazcopy-v10-linux
tar -xf azcopy.tar.gz --strip-components=1 tar -xf azcopy.tar.gz --strip-components=1
mkdir -p dist mkdir -p dist
# Our container is public, so the SAS token should not be needed ./azcopy cp "https://$AZURE_ACCOUNT_NAME.blob.core.windows.net/$AZURE_STORAGE_CONTAINER/$DIRECTORY_NAME/*" "dist"
# But AzCopy is broken https://github.com/Azure/azure-storage-azcopy/issues/971
./azcopy cp "https://$AZURE_ACCOUNT_NAME.blob.core.windows.net/$AZURE_STORAGE_CONTAINER/$DIRECTORY_NAME/*?sv=2019-02-02&ss=b&srt=co&sp=rl&se=2100-04-21T15:00:00Z&st=2020-04-21T19:07:13Z&spr=https&sig=5hMGP4ZR3MUVVp4AVxFDS%2BuFY%2FsU4M8%2B2wKOr8utpWI%3D" \
"dist"
release="$(cat Build/RELEASE.md)" release="$(cat Build/RELEASE.md)"
version="$(echo "$TRAVIS_TAG" | cut -d'/' -f2)" version="$(echo "$GITHUB_REF_NAME" | cut -d'/' -f2)"
payload="$(jq -M --arg "tag_name" "$TRAVIS_TAG" \ payload="$(jq -M --arg "tag_name" "$GITHUB_REF_NAME" \
--arg "name" "BTCPayServer Vault $version" \ --arg "name" "BTCPayServer Vault $version" \
--arg "body" "$release" \ --arg "body" "$release" \
--argjson "draft" false \ --argjson "draft" $draft \
--argjson "prerelease" true \ --argjson "prerelease" $prerelease \
'. | .tag_name=$tag_name | .name=$name | .body=$body | .draft=$draft | .prerelease=$prerelease' \ '. | .tag_name=$tag_name | .name=$name | .body=$body | .draft=$draft | .prerelease=$prerelease' \
<<<'{}')" <<<'{}')"
echo "Creating release to https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases" echo "Creating release to https://api.github.com/repos/$GITHUB_REPOSITORY/releases"
echo "$payload" echo "$payload"
response="$(curl --fail -s -S -X POST https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases \ response="$(curl --fail -s -S -X POST https://api.github.com/repos/$GITHUB_REPOSITORY/releases \
-H "Accept: application/vnd.github.v3+json" \ -H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token $GITHUB_TOKEN" \ -H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@ -61,5 +66,5 @@ for f in *; do
-H "Authorization: token $GITHUB_TOKEN" \ -H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: $media_type" \ -H "Content-Type: $media_type" \
--data-binary @"$f" \ --data-binary @"$f" \
"https://uploads.github.com/repos/$TRAVIS_REPO_SLUG/releases/$release_id/assets?name=$f" "https://uploads.github.com/repos/$GITHUB_REPOSITORY/releases/$release_id/assets?name=$f"
done done

View File

@ -11,14 +11,12 @@ if ! [[ "$PGP_KEY" ]]; then
fi fi
AZURE_ACCOUNT_NAME="$(echo "$AZURE_STORAGE_CONNECTION_STRING" | cut -d'=' -f3 | cut -d';' -f1)" AZURE_ACCOUNT_NAME="$(echo "$AZURE_STORAGE_CONNECTION_STRING" | cut -d'=' -f3 | cut -d';' -f1)"
DIRECTORY_NAME="dist-$TRAVIS_BUILD_ID" DIRECTORY_NAME="dist-$GITHUB_RUN_ID"
wget -O azcopy.tar.gz https://aka.ms/downloadazcopy-v10-linux wget -O azcopy.tar.gz https://aka.ms/downloadazcopy-v10-linux
tar -xf azcopy.tar.gz --strip-components=1 tar -xf azcopy.tar.gz --strip-components=1
mkdir -p dist mkdir -p dist
# Our container is public, so the SAS token should not be needed ./azcopy cp "https://$AZURE_ACCOUNT_NAME.blob.core.windows.net/$AZURE_STORAGE_CONTAINER/$DIRECTORY_NAME/*" "dist"
# But AzCopy is broken https://github.com/Azure/azure-storage-azcopy/issues/971
./azcopy cp "https://$AZURE_ACCOUNT_NAME.blob.core.windows.net/$AZURE_STORAGE_CONTAINER/$DIRECTORY_NAME/*?sv=2019-02-02&ss=b&srt=co&sp=rl&se=2100-04-21T15:00:00Z&st=2020-04-21T19:07:13Z&spr=https&sig=5hMGP4ZR3MUVVp4AVxFDS%2BuFY%2FsU4M8%2B2wKOr8utpWI%3D" \
"dist"
cd dist cd dist
for f in *; do for f in *; do
if [[ "$f" == "SHA256SUMS" ]]; then continue; fi if [[ "$f" == "SHA256SUMS" ]]; then continue; fi
@ -27,7 +25,8 @@ done
mv /tmp/SHA256SUMS SHA256SUMS mv /tmp/SHA256SUMS SHA256SUMS
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
echo "$PGP_KEY" | base64 --decode | gpg --import echo "$PGP_KEY" | base64 --decode | gpg --import --no-tty
gpg --digest-algo sha256 --clearsign SHA256SUMS echo "PGP keys correctly imported"
gpg --no-tty --digest-algo sha256 --clearsign SHA256SUMS
az storage blob upload -f "SHA256SUMS.asc" -c "$AZURE_STORAGE_CONTAINER" -n "$DIRECTORY_NAME/SHA256SUMS.asc" az storage blob upload -f "SHA256SUMS.asc" -c "$AZURE_STORAGE_CONTAINER" -n "$DIRECTORY_NAME/SHA256SUMS.asc"
rm SHA256SUMS rm SHA256SUMS

View File

View File

@ -9,6 +9,10 @@ The process to publish a new version is the following:
The build system relies on docker to build the packages. The build system relies on docker to build the packages.
Each dockerfile in `<rid>/Dockerfile` will generate a docker image with the package in it. Each dockerfile in `<rid>/Dockerfile` will generate a docker image with the package in it.
## Test releases
You can test a release by tagging with `Vault/v[VERSION]-test`. This will create a draft pre release.
## How to test Debian ## How to test Debian
The debian package is easy to test, run: The debian package is easy to test, run:

View File

@ -1,5 +1,7 @@
## Changelog ## Changelog
* Bump of hwi to 1.2.1 * Update to HWI 3.2.0
* Update to .NET 10.0
* Simplify setup for arch linux
You may want to follow the [documented](https://github.com/btcpayserver/BTCPayServer.Vault/blob/master/docs/HowToVerify.md) process to verify that the binaries are built by Nicolas Dorier. You may want to follow the [documented](https://github.com/btcpayserver/BTCPayServer.Vault/blob/master/docs/HowToVerify.md) process to verify that the binaries are built by Nicolas Dorier.

View File

@ -1,10 +1,12 @@
#!/bin/bash #!/bin/bash
BUILD_ARGS="--runtime $RUNTIME -p:Configuration=Release -p:GithubDistrib=true" DOTNET_RUNTIME=${DOTNET_RUNTIME:-$RUNTIME}
FRAMEWORK="netcoreapp3.1" BUILD_ARGS="--runtime $DOTNET_RUNTIME -p:Configuration=Release -p:GithubDistrib=true"
FRAMEWORK="net10.0"
DIST="/source/dist" DIST="/source/dist"
RESOURCES="/source/Build/${RUNTIME}" RESOURCES="/source/Build/${RUNTIME}"
RESOURCES_COMMON="/source/Build/common" RESOURCES_COMMON="/source/Build/common"
RESOURCES_LINUX="/source/Build/linux-x64"
PROJECT_FILE="/source/BTCPayServer.Vault/BTCPayServer.Vault.csproj" PROJECT_FILE="/source/BTCPayServer.Vault/BTCPayServer.Vault.csproj"
VERSION_FILE="/source/BTCPayServer.Vault/Version.csproj" VERSION_FILE="/source/BTCPayServer.Vault/Version.csproj"
LICENSE="$(cat $PROJECT_FILE | sed -n 's/.*<PackageLicenseExpression>\(.*\)<\/PackageLicenseExpression>.*/\1/p')" LICENSE="$(cat $PROJECT_FILE | sed -n 's/.*<PackageLicenseExpression>\(.*\)<\/PackageLicenseExpression>.*/\1/p')"
@ -14,7 +16,7 @@ TITLE="$(cat $PROJECT_FILE | sed -n 's/.*<Title>\(.*\)<\/Title>.*/\1/p')"
if [ -f "$VERSION_FILE" ]; then if [ -f "$VERSION_FILE" ]; then
VERSION="$(cat $VERSION_FILE | sed -n 's/.*<Version>\(.*\)<\/Version>.*/\1/p')" VERSION="$(cat $VERSION_FILE | sed -n 's/.*<Version>\(.*\)<\/Version>.*/\1/p')"
fi fi
PUBLISH_FOLDER="/source/BTCPayServer.Vault/bin/Release/$FRAMEWORK/$RUNTIME/publish" PUBLISH_FOLDER="/source/BTCPayServer.Vault/bin/Release/$FRAMEWORK/$DOTNET_RUNTIME/publish"
EXECUTABLE="$(cat $PROJECT_FILE | sed -n 's/.*<TargetName>\(.*\)<\/TargetName>.*/\1/p')" EXECUTABLE="$(cat $PROJECT_FILE | sed -n 's/.*<TargetName>\(.*\)<\/TargetName>.*/\1/p')"
mkdir -p "$DIST" mkdir -p "$DIST"

View File

@ -1,13 +1,13 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder
# Optimize docker cache, do not make it one layer # Optimize docker cache, do not make it one layer
RUN apt-get update RUN apt-get update
RUN apt-get install -y --no-install-recommends imagemagick RUN apt-get install -y --no-install-recommends imagemagick
### ###
RUN wget -qO /tmp/hwi.tar.gz https://github.com/bitcoin-core/HWI/releases/download/1.2.1/hwi-1.2.1-linux-amd64.tar.gz && \ RUN wget -qO /tmp/hwi.tar.gz https://github.com/bitcoin-core/HWI/releases/download/3.2.0/hwi-3.2.0-linux-x86_64.tar.gz && \
tar -zxvf /tmp/hwi.tar.gz -C /tmp hwi && \ tar -zxvf /tmp/hwi.tar.gz -C /tmp hwi && \
echo "23ea301117f74561294b5b3ebe1eeb461004aff7e479c4b90a0aaec5924cc677 /tmp/hwi" | sha256sum -c - && \ echo "d9cc65de95e3cf93fd3c953d589184a00180624ffc5ad17aade97616a8919fa6 /tmp/hwi" | sha256sum -c - && \
rm /tmp/hwi.tar.gz rm /tmp/hwi.tar.gz
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-c"]
@ -15,8 +15,10 @@ ARG PGP_KEY=""
RUN ! [[ "${PGP_KEY}" ]] || apt-get install -y debsigs RUN ! [[ "${PGP_KEY}" ]] || apt-get install -y debsigs
WORKDIR /source WORKDIR /source
ENV DOTNET_RUNTIME "linux-x64"
ENV RUNTIME "debian-x64" ENV RUNTIME "debian-x64"
COPY "Build/common" "Build/common" COPY "Build/common" "Build/common"
COPY "Build/linux-x64" "Build/linux-x64"
ENV EXPORT_VARIABLES "source Build/common/export-variables.sh" ENV EXPORT_VARIABLES "source Build/common/export-variables.sh"
COPY BTCPayServer.Vault/BTCPayServer.Vault.csproj BTCPayServer.Vault/BTCPayServer.Vault.csproj COPY BTCPayServer.Vault/BTCPayServer.Vault.csproj BTCPayServer.Vault/BTCPayServer.Vault.csproj
COPY BTCPayServer.Hwi/BTCPayServer.Hwi.csproj BTCPayServer.Hwi/BTCPayServer.Hwi.csproj COPY BTCPayServer.Hwi/BTCPayServer.Hwi.csproj BTCPayServer.Hwi/BTCPayServer.Hwi.csproj
@ -28,6 +30,7 @@ RUN $EXPORT_VARIABLES && dotnet_publish && mv /tmp/hwi "$PUBLISH_FOLDER/"
COPY "Build/${RUNTIME}" "Build/${RUNTIME}" COPY "Build/${RUNTIME}" "Build/${RUNTIME}"
COPY BTCPayServerVault.png BTCPayServerVault.png COPY BTCPayServerVault.png BTCPayServerVault.png
RUN $EXPORT_VARIABLES && \ RUN $EXPORT_VARIABLES && \
find "$PUBLISH_FOLDER" -type f -exec chmod 644 {} \; && \ find "$PUBLISH_FOLDER" -type f -exec chmod 644 {} \; && \
find "$PUBLISH_FOLDER" -type f \( -name 'hwi' -o -name "$EXECUTABLE" \) -exec chmod +x {} \; && \ find "$PUBLISH_FOLDER" -type f \( -name 'hwi' -o -name "$EXECUTABLE" \) -exec chmod +x {} \; && \
@ -49,7 +52,7 @@ RUN $EXPORT_VARIABLES && \
cp "$RESOURCES/BTCPayServer.Vault.desktop" "$debiandir/usr/share/applications/" && \ cp "$RESOURCES/BTCPayServer.Vault.desktop" "$debiandir/usr/share/applications/" && \
replaceProjectVariables "$debiandir/usr/share/applications/BTCPayServer.Vault.desktop" && \ replaceProjectVariables "$debiandir/usr/share/applications/BTCPayServer.Vault.desktop" && \
mkdir -p "$debiandir/lib/udev/rules.d" && \ mkdir -p "$debiandir/lib/udev/rules.d" && \
cp $RESOURCES/udev/* "$debiandir/lib/udev/rules.d/" && \ cp $RESOURCES_LINUX/udev/* "$debiandir/lib/udev/rules.d/" && \
sizeinkb="$(du -k --max-depth=0 $debiandir | cut -f 1)" && \ sizeinkb="$(du -k --max-depth=0 $debiandir | cut -f 1)" && \
sed -i "s/{SIZEINKB}/$sizeinkb/g" "$debiandir/DEBIAN/control" && \ sed -i "s/{SIZEINKB}/$sizeinkb/g" "$debiandir/DEBIAN/control" && \
dpkg --build "$debiandir" && mv /tmp/debian.deb "$DIST/BTCPayServerVault-$VERSION.deb" && \ dpkg --build "$debiandir" && mv /tmp/debian.deb "$DIST/BTCPayServerVault-$VERSION.deb" && \

View File

@ -7,5 +7,6 @@ Vcs-Git: git://github.com/btcpayserver/BTCPayServer.Vault.git
Vcs-Browser: https://github.com/btcpayserver/BTCPayServer.Vault Vcs-Browser: https://github.com/btcpayserver/BTCPayServer.Vault
Architecture: amd64 Architecture: amd64
License: Open Source (MIT) License: Open Source (MIT)
Depends: libgcc1, libicu | libicu76 | libicu74 | libicu72 | libicu71 | libicu70 | libicu69 | libicu68 | libicu67 | libicu66 | libicu65 | libicu63 | libicu60 | libicu57 | libicu55 | libicu52, libc6, libssl1.0.0 | libssl1.0.2 | libssl1.1 | libssl3, zlib1g, libstdc++6, libgssapi-krb5-2, libpcsclite1
Installed-Size: {SIZEINKB} Installed-Size: {SIZEINKB}
Description: {DESCRIPTION} Description: {DESCRIPTION}

View File

@ -1,9 +0,0 @@
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="2b7c", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="3b7c", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="4b7c", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1807", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1808", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001", MODE="0660", GROUP="plugdev"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004", MODE="0660", GROUP="plugdev"

View File

@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=BTCPayServer Vault
Exec=/usr/local/bin/BTCPayServer.Vault
Icon=BTCPayServerVault
Categories=Utility;

View File

@ -1,13 +1,18 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder
RUN wget -qO /tmp/hwi.tar.gz https://github.com/bitcoin-core/HWI/releases/download/1.2.1/hwi-1.2.1-linux-amd64.tar.gz && \ RUN wget -qO /tmp/hwi.tar.gz https://github.com/bitcoin-core/HWI/releases/download/3.2.0/hwi-3.2.0-linux-x86_64.tar.gz && \
tar -zxvf /tmp/hwi.tar.gz -C /tmp hwi && \ tar -zxvf /tmp/hwi.tar.gz -C /tmp hwi && \
echo "23ea301117f74561294b5b3ebe1eeb461004aff7e479c4b90a0aaec5924cc677 /tmp/hwi" | sha256sum -c - && \ echo "d9cc65de95e3cf93fd3c953d589184a00180624ffc5ad17aade97616a8919fa6 /tmp/hwi" | sha256sum -c - && \
rm /tmp/hwi.tar.gz rm /tmp/hwi.tar.gz
RUN apt-get update && \
apt-get install -y --no-install-recommends imagemagick && \
rm -rf /var/lib/apt/lists/*
WORKDIR /source WORKDIR /source
ENV RUNTIME "linux-x64" ENV RUNTIME "linux-x64"
COPY "Build/common" "Build/common" COPY "Build/common" "Build/common"
COPY "Build/linux-x64" "Build/linux-x64"
ENV EXPORT_VARIABLES "source Build/common/export-variables.sh" ENV EXPORT_VARIABLES "source Build/common/export-variables.sh"
COPY BTCPayServer.Vault/BTCPayServer.Vault.csproj BTCPayServer.Vault/BTCPayServer.Vault.csproj COPY BTCPayServer.Vault/BTCPayServer.Vault.csproj BTCPayServer.Vault/BTCPayServer.Vault.csproj
COPY BTCPayServer.Hwi/BTCPayServer.Hwi.csproj BTCPayServer.Hwi/BTCPayServer.Hwi.csproj COPY BTCPayServer.Hwi/BTCPayServer.Hwi.csproj BTCPayServer.Hwi/BTCPayServer.Hwi.csproj
@ -18,10 +23,17 @@ COPY BTCPayServer.Hwi BTCPayServer.Hwi
COPY BTCPayServer.Vault BTCPayServer.Vault COPY BTCPayServer.Vault BTCPayServer.Vault
RUN $EXPORT_VARIABLES && dotnet_publish && mv /tmp/hwi "$PUBLISH_FOLDER/" RUN $EXPORT_VARIABLES && dotnet_publish && mv /tmp/hwi "$PUBLISH_FOLDER/"
COPY "BTCPayServerVault.png" "BTCPayServerVault.png"
RUN $EXPORT_VARIABLES && \ RUN $EXPORT_VARIABLES && \
cp -r "$RESOURCES_LINUX/udev" "$PUBLISH_FOLDER/" && \
cp -r $RESOURCES_LINUX/install-*.sh "$PUBLISH_FOLDER/" && \
cp -r "$RESOURCES_LINUX/BTCPayServerVault.desktop" "$PUBLISH_FOLDER/" && \
convert -background none -resize "64x64" "BTCPayServerVault.png" "/tmp/BTCPayServerVault.png" && \
cp "/tmp/BTCPayServerVault.png" "$PUBLISH_FOLDER/" && \
find "$PUBLISH_FOLDER" -type f -exec chmod 644 {} \; && \ find "$PUBLISH_FOLDER" -type f -exec chmod 644 {} \; && \
find "$PUBLISH_FOLDER" -type f \( -name 'hwi' -o -name "$EXECUTABLE" \) -exec chmod +x {} \; && \ find "$PUBLISH_FOLDER" -type f \( -name 'hwi' -o -name "$EXECUTABLE" -o -name '*.sh' \) -exec chmod +x {} \; && \
# We need to cd in "$PUBLISH_FOLDER", because tar's -C option always add a root folder to the tar otherwise # We need to cd in "$PUBLISH_FOLDER", because tar's -C option always add a root folder to the tar otherwise
cd "$PUBLISH_FOLDER" && tar -czf "$DIST/BTCPayServerVault-Linux-$VERSION.tar.gz" * cd "$PUBLISH_FOLDER" && tar -czf "$DIST/BTCPayServerVault-Linux-$VERSION.tar.gz" *
ENTRYPOINT [ "/bin/bash", "-c", "$EXPORT_VARIABLES && cp $DIST/* /opt/dist/" ] ENTRYPOINT [ "/bin/bash", "-c", "$EXPORT_VARIABLES && cp -a $DIST/* /opt/dist/" ]

28
Build/linux-x64/install-arch.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root." >&2
exit 1
fi
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
RULES_DIR="$SCRIPT_DIR/udev"
install -Dm644 "$SCRIPT_DIR/BTCPayServerVault.desktop" /usr/share/applications/BTCPayServerVault.desktop
install -Dm644 "$SCRIPT_DIR/BTCPayServerVault.png" /usr/share/icons/hicolor/64x64/apps/BTCPayServerVault.png
rm -rf /opt/BTCPayServer.Vault
mkdir -p /opt/BTCPayServer.Vault
cp -r "$SCRIPT_DIR"/. "/opt/BTCPayServer.Vault/"
chmod +x /opt/BTCPayServer.Vault/BTCPayServer.Vault
echo "/opt/BTCPayServer.Vault created"
ln -sfnT /opt/BTCPayServer.Vault/BTCPayServer.Vault /usr/local/bin/BTCPayServer.Vault
chmod +x /usr/local/bin/BTCPayServer.Vault
echo "/usr/local/bin/BTCPayServer.Vault created"
echo "If the Vault cannot access your hardware wallet, you may need to restart your computer."

View File

@ -0,0 +1,12 @@
# HW.1 / Nano
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c|2b7c|3b7c|4b7c", TAG+="uaccess", TAG+="udev-acl"
# Blue
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0000|0000|0001|0002|0003|0004|0005|0006|0007|0008|0009|000a|000b|000c|000d|000e|000f|0010|0011|0012|0013|0014|0015|0016|0017|0018|0019|001a|001b|001c|001d|001e|001f", TAG+="uaccess", TAG+="udev-acl"
# Nano S
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0001|1000|1001|1002|1003|1004|1005|1006|1007|1008|1009|100a|100b|100c|100d|100e|100f|1010|1011|1012|1013|1014|1015|1016|1017|1018|1019|101a|101b|101c|101d|101e|101f", TAG+="uaccess", TAG+="udev-acl"
# Aramis
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0002|2000|2001|2002|2003|2004|2005|2006|2007|2008|2009|200a|200b|200c|200d|200e|200f|2010|2011|2012|2013|2014|2015|2016|2017|2018|2019|201a|201b|201c|201d|201e|201f", TAG+="uaccess", TAG+="udev-acl"
# HW2
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0003|3000|3001|3002|3003|3004|3005|3006|3007|3008|3009|300a|300b|300c|300d|300e|300f|3010|3011|3012|3013|3014|3015|3016|3017|3018|3019|301a|301b|301c|301d|301e|301f", TAG+="uaccess", TAG+="udev-acl"
# Nano X
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004|4000|4001|4002|4003|4004|4005|4006|4007|4008|4009|400a|400b|400c|400d|400e|400f|4010|4011|4012|4013|4014|4015|4016|4017|4018|4019|401a|401b|401c|401d|401e|401f", TAG+="uaccess", TAG+="udev-acl"

View File

@ -1,3 +1,11 @@
# Linux udev support file.
#
# This is a example udev file for HIDAPI devices which changes the permissions
# to 0666 (world readable/writable) for a specific device on Linux systems.
#
# - Copy this file into /etc/udev/rules.d and unplug and re-plug your Coldcard.
# - Udev does not have to be restarted.
#
# probably not needed: # probably not needed:
SUBSYSTEMS=="usb", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666"
@ -5,4 +13,3 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plu
# required: # required:
# from <https://github.com/signal11/hidapi/blob/master/udev/99-hid.rules> # from <https://github.com/signal11/hidapi/blob/master/udev/99-hid.rules>
KERNEL=="hidraw*", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666" KERNEL=="hidraw*", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666"

View File

@ -1,4 +1,4 @@
# TREZOR: The Original Hardware Wallet # Trezor: The Original Hardware Wallet
# https://trezor.io/ # https://trezor.io/
# #
# Put this file into /etc/udev/rules.d # Put this file into /etc/udev/rules.d
@ -7,11 +7,11 @@
# put this into /usr/lib/udev/rules.d or /lib/udev/rules.d # put this into /usr/lib/udev/rules.d or /lib/udev/rules.d
# depending on your distribution # depending on your distribution
# TREZOR # Trezor
SUBSYSTEM=="usb", ATTR{idVendor}=="534c", ATTR{idProduct}=="0001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n" SUBSYSTEM=="usb", ATTR{idVendor}=="534c", ATTR{idProduct}=="0001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n"
KERNEL=="hidraw*", ATTRS{idVendor}=="534c", ATTRS{idProduct}=="0001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" KERNEL=="hidraw*", ATTRS{idVendor}=="534c", ATTRS{idProduct}=="0001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"
# TREZOR v2 # Trezor v2
SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="53c0", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n" SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="53c0", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n"
SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="53c1", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n" SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="53c1", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n"
KERNEL=="hidraw*", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="53c1", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" KERNEL=="hidraw*", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="53c1", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"

View File

@ -8,4 +8,4 @@ KERNEL=="hidraw*", ATTRS{idVendor}=="2b24", ATTRS{idProduct}=="0001", MODE="066
# KeepKey WebUSB Firmware/Bootloader # KeepKey WebUSB Firmware/Bootloader
SUBSYSTEM=="usb", ATTR{idVendor}=="2b24", ATTR{idProduct}=="0002", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="keepkey%n" SUBSYSTEM=="usb", ATTR{idVendor}=="2b24", ATTR{idProduct}=="0002", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="keepkey%n"
KERNEL=="hidraw*", ATTRS{idVendor}=="2b24", ATTRS{idProduct}=="0002", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" KERNEL=="hidraw*", ATTRS{idVendor}=="2b24", ATTRS{idProduct}=="0002", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder
# Optimize docker cache, do not make it one layer # Optimize docker cache, do not make it one layer
RUN apt-get update RUN apt-get update
@ -7,9 +7,9 @@ RUN apt-get install -y --no-install-recommends imagemagick
RUN apt-get install -y --no-install-recommends git icnsutils RUN apt-get install -y --no-install-recommends git icnsutils
RUN wget -qO /tmp/hwi.tar.gz https://github.com/bitcoin-core/HWI/releases/download/1.2.1/hwi-1.2.1-mac-amd64.tar.gz && \ RUN wget -qO /tmp/hwi.tar.gz https://github.com/bitcoin-core/HWI/releases/download/3.2.0/hwi-3.2.0-mac-x86_64.tar.gz && \
tar -zxvf /tmp/hwi.tar.gz -C /tmp hwi && \ tar -zxvf /tmp/hwi.tar.gz -C /tmp hwi && \
echo "dc516e563db7c0f21b3f017313fc93a2a57f8d614822b8c71f1467a4e5f59dbb /tmp/hwi" | sha256sum -c - && \ echo "b3764a530b635e7a7348c9185e09e74b389f5f585094fe316f700eec7c761875 /tmp/hwi" | sha256sum -c - && \
rm /tmp/hwi.tar.gz rm /tmp/hwi.tar.gz
WORKDIR /source WORKDIR /source

View File

@ -10,5 +10,7 @@
<true/> <true/>
<key>com.apple.security.cs.disable-library-validation</key> <key>com.apple.security.cs.disable-library-validation</key>
<true/> <true/>
<key>com.apple.security.smartcard</key>
<true/>
</dict> </dict>
</plist> </plist>

11
Build/push-new-tag.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
version_file="$script_dir/../BTCPayServer.Vault/Version.csproj"
ver="$(grep -oPm1 '(?<=<Version>)[^<]+' "$version_file")"
tag="Vault/v$ver"
git tag -a "$tag" -m "$tag"
git push origin "$tag"

View File

@ -1,47 +0,0 @@
#!/bin/bash
set -e
DOCKER_IMAGE_NAME="vault-$RID"
docker build -t "$DOCKER_IMAGE_NAME" $DOCKER_BUILD_ARGS -f "Build/$RID/Dockerfile" .
docker run --rm -v "$(pwd)/dist:/opt/dist" "$DOCKER_IMAGE_NAME"
if [[ "$TRAVIS_TAG" ]]; then
travis_version="$(echo "$TRAVIS_TAG" | cut -d'/' -f2)"
csproj_version="v$(cat BTCPayServer.Vault/Version.csproj | sed -n 's/.*<Version>\(.*\)<\/Version>.*/\1/p')"
if [[ "$travis_version" != "$csproj_version" ]]; then
echo "The tagged version on travis ($travis_version) is different from the csproj ($csproj_version)"
exit 1
fi
fi
if ! [[ "$AZURE_STORAGE_CONNECTION_STRING" ]] || ! [[ "$AZURE_STORAGE_CONTAINER" ]]; then
echo "Skipped upload to Azure (AZURE_STORAGE_CONNECTION_STRING or AZURE_STORAGE_CONTAINER not set)"
exit 0
fi
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
az storage container create --name "$AZURE_STORAGE_CONTAINER" --public-access "container"
sudo apt-get install -y ccrypt
sudo touch dist/secrets
sudo chmod 777 dist/secrets
echo "APPLE_DEV_ID_CERT='$APPLE_DEV_ID_CERT'" >> dist/secrets
echo "APPLE_DEV_ID_CERT_PASSWORD='$APPLE_DEV_ID_CERT_PASSWORD'" >> dist/secrets
echo "APPLE_ID='$APPLE_ID'" >> dist/secrets
echo "APPLE_ID_PASSWORD='$APPLE_ID_PASSWORD'" >> dist/secrets
echo "AZURE_STORAGE_CONNECTION_STRING='$AZURE_STORAGE_CONNECTION_STRING'" >> dist/secrets
echo "AZURE_STORAGE_CONTAINER='$AZURE_STORAGE_CONTAINER'" >> dist/secrets
echo "GITHUB_TOKEN='$GITHUB_TOKEN'" >> dist/secrets
echo "PGP_KEY='$PGP_KEY'" >> dist/secrets
echo "WINDOWS_CERT='$WINDOWS_CERT'" >> dist/secrets
echo "WINDOWS_CERT_PASSWORD='$WINDOWS_CERT_PASSWORD'" >> dist/secrets
sudo ccencrypt dist/secrets -K $ENC_KEY
for file in dist/*; do
BLOB_NAME="dist-$TRAVIS_BUILD_ID/$(basename -- $file)"
echo "Uploading $BLOB_NAME"
az storage blob upload -f "$file" -c "$AZURE_STORAGE_CONTAINER" -n "$BLOB_NAME"
url="$(az storage blob url --container-name "$AZURE_STORAGE_CONTAINER" --name "$BLOB_NAME" --protocol "https")"
echo "Uploaded file to $url"
done

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder
# Optimize docker cache, do not make it one layer # Optimize docker cache, do not make it one layer
RUN apt-get update RUN apt-get update
@ -6,9 +6,9 @@ RUN apt-get install -y --no-install-recommends imagemagick
### ###
RUN apt-get install -y --no-install-recommends nsis unzip wine xxd osslsigncode openssl RUN apt-get install -y --no-install-recommends nsis unzip wine xxd osslsigncode openssl
RUN wget -qO "/tmp/hwi.zip" https://github.com/bitcoin-core/HWI/releases/download/1.2.1/hwi-1.2.1-windows-amd64.zip && \ RUN wget -qO "/tmp/hwi.zip" https://github.com/bitcoin-core/HWI/releases/download/3.2.0/hwi-3.2.0-windows-x86_64.zip && \
unzip "/tmp/hwi.zip" -d "/tmp" && \ unzip "/tmp/hwi.zip" -d "/tmp" && \
echo "b8b21499592a311cfaa18676280807d6bf674d72cef21409ed265069f6582c1b /tmp/hwi.exe" | sha256sum -c - && \ echo "e068d91b664597425a8ead02d7b86a02ad6c4b72746c42961f58a58b08f9fd79 /tmp/hwi.exe" | sha256sum -c - && \
rm "/tmp/hwi.zip" && \ rm "/tmp/hwi.zip" && \
# Need to setup with rcedit because https://github.com/dotnet/sdk/issues/3943 # Need to setup with rcedit because https://github.com/dotnet/sdk/issues/3943
# I prebuild the binaries with VS 2019 on commit b807b34a644c86c0b0d89c7f073967e79202731a # I prebuild the binaries with VS 2019 on commit b807b34a644c86c0b0d89c7f073967e79202731a
@ -39,14 +39,6 @@ RUN $EXPORT_VARIABLES && \
done && \ done && \
convert /tmp/BTCPayServerVault.ico.tmp/*.png /tmp/BTCPayServerVault.ico && \ convert /tmp/BTCPayServerVault.ico.tmp/*.png /tmp/BTCPayServerVault.ico && \
executable="$PUBLISH_FOLDER/$EXECUTABLE.exe" && \ executable="$PUBLISH_FOLDER/$EXECUTABLE.exe" && \
# Workaround https://github.com/dotnet/sdk/issues/3990
e_lfanew_loc=$(xxd -p -l1 -s $((16#3C)) "$executable") && \
e_lfanew_loc="$((16#$e_lfanew_loc))" && \
echo "e_lfanew=$(printf "%08x" 0x$e_lfanew_loc)" && \
subsystem_loc=$(($e_lfanew_loc + 92)) && \
echo "subsystem_loc location is 0x$(printf "%08x" $subsystem_loc)" && \
printf "%06x: 0200" $subsystem_loc | xxd -r - "$executable" && \
# End of workaround
wine /tmp/rcedit.exe "$executable" \ wine /tmp/rcedit.exe "$executable" \
--set-icon "/tmp/BTCPayServerVault.ico" \ --set-icon "/tmp/BTCPayServerVault.ico" \
--set-version-string "LegalCopyright" "$LICENSE" \ --set-version-string "LegalCopyright" "$LICENSE" \

View File

@ -1,16 +1,68 @@
[![NuGet](https://img.shields.io/nuget/v/BTCPayServer.Hwi.svg)](https://www.nuget.org/packages/BTCPayServer.Hwi) [![Build Status](https://travis-ci.org/btcpayserver/BTCPayServer.Vault.svg?branch=master)](https://travis-ci.org/btcpayserver/BTCPayServer.Vault) [![NuGet](https://img.shields.io/nuget/v/BTCPayServer.Hwi.svg)](https://www.nuget.org/packages/BTCPayServer.Hwi) [![Build status](https://github.com/btcpayserver/BTCPayServer.Vault/workflows/CI/badge.svg)](https://github.com/btcpayserver/BTCPayServer.Vault/actions?query=workflow%3ACI)
# BTCPayServer.Vault # BTCPayServer.Vault
This project is composed of two parts: This project is composed of two parts:
* [BTCPayServer.Hwi](BTCPayServer.Hwi): An easy to use library ([nuget](https://www.nuget.org/packages/BTCPayServer.Hwi)) wrapping the command line interface of the [hwi project](https://github.com/bitcoin-core/HWI). * [BTCPayServer.Hwi](https://github.com/btcpayserver/BTCPayServer.Vault/tree/master/BTCPayServer.Hwi): An easy to use library ([nuget](https://www.nuget.org/packages/BTCPayServer.Hwi)) wrapping the command line interface of the [hwi project](https://github.com/bitcoin-core/HWI).
* [BTCPayServer.Vault](BTCPayServer.Vault): A simple local web server providing access to the hardware wallet physically connected to your computer via hwi. * [BTCPayServer.Vault](https://github.com/btcpayserver/BTCPayServer.Vault/tree/master/BTCPayServer.Vault): A simple local web server providing access to the hardware wallet physically connected to your computer via hwi.
The video below explains how to use BTCPay Vault with BTCPay Server.
[![](https://img.youtube.com/vi/s4qbGxef43A/mqdefault.jpg)](https://www.youtube.com/watch?v=s4qbGxef43A)
- [BTCPay Vault announcement](https://blog.btcpayserver.org/btcpay-vault/)
## Why BTCPayServer Vault ## Why BTCPayServer Vault
BTCPayServer Vault allows web applications to access your hardware wallet, this enables a better integrated user experience. BTCPayServer Vault allows web applications to access your hardware wallet, this enables a better integrated user experience.
## How to install
### Direct download
The binaries are on our [release page](https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest).
### Via brew (macOS only)
You can use brew:
```bash
brew install btcpayserver-vault
```
### On Arch Linux
Download the tarball on our [release page](https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest)
```bash
tar -xvf <tarball.tar.gz>
sudo ./install-arch.sh
```
If BTCPay Server fails to detect your hardware wallet, you may need to restart.
Check if you try to run the `hwi` executable. If not, install python9 dependencies, and run
```bash
ln -s /usr/lib/libcrypt.so.2 /usr/lib/libcrypt.so.1
```
### On Debian
Download the `.deb` package on our [release page](https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest)
```bash
sudo apt install <package.deb>
```
### Other linux
Inspire you from [install-arch.sh](Build/linux-x64/install-arch.sh).
We provide the udev rules and desktop entries in the tarball.
## How does BTCPayServer Vault work ## How does BTCPayServer Vault work
When running the BTCPayServer Vault, a local webserver is hosted on `http://127.0.0.1:65092` which web applications, via your local browser, can connect to in order to interact with your hardware wallet. When running the BTCPayServer Vault, a local webserver is hosted on `http://127.0.0.1:65092` which web applications, via your local browser, can connect to in order to interact with your hardware wallet.
@ -45,7 +97,7 @@ This is why BTCPayServer Vault always ask permission to user first before allowi
This is a two step process: This is a two step process:
1. Install the latest version of the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1) 1. Install the latest version of the [.NET Core 6.0 SDK](https://dotnet.microsoft.com/download/dotnet-core/6.0)
2. Run `dotnet build` 2. Run `dotnet build`
If you want to run it for testing: If you want to run it for testing:
@ -59,10 +111,10 @@ dotnet run
Video below explains how to use BTCPay Vault with BTCPay Server. Video below explains how to use BTCPay Vault with BTCPay Server.
[![](https://img.youtube.com/vi/hh_cm8MKl2g/mqdefault.jpg)](https://www.youtube.com/watch?v=hh_cm8MKl2g) [![](https://img.youtube.com/vi/s4qbGxef43A/mqdefault.jpg)](https://www.youtube.com/watch?v=s4qbGxef43A)
- [BTCPay Vault announcement](https://blog.btcpayserver.org/btcpay-vault/) - [BTCPay Vault announcement](https://blog.btcpayserver.org/btcpay-vault/)
- [Using BTCPay Vault with BTCPay Server](https://docs.btcpayserver.org/features/vault) - [Using BTCPay Vault with BTCPay Server](https://docs.btcpayserver.org/Vault)
## Licence ## Licence

View File

@ -1,27 +1,25 @@
# How to verify release binaries signatures # How to verify release signatures
## Introduction ## Introduction
Downloading binaries on internet might be dangerous. When you download the binaries of the release of BTCPayServer Vault on our github release page, you only ensure that the uploader had access to our github repository. Downloading binaries from the internet might be dangerous. When you download a release of BTCPayServer Vault on our [GitHub releases page](https://github.com/btcpayserver/BTCPayServer.Vault/releases), you only ensure that the uploader had access to our GitHub repository.
This might be fine, but sometimes you download the same binaries from a different source, or you want additional assurance that those binaries are signed by the developers of the project. (In this case, Nicolas Dorier) This might be fine, but sometimes you download the same binaries from a different source, or you want additional assurance that those binaries are signed by the developers of the project. (In this case, Nicolas Dorier)
If you do not care about who signed the executable and verifying the integrity of the files you downloaded, you don't have to read this document. If you do not care about who signed the executable and verifying the integrity of the files you downloaded, you don't have to read this document.
## Cheking PGP signatures<a name="pgp"></a> ## Checking PGP signatures<a name="pgp"></a>
For this you need the `gpg` tool, make sure it is installed on your machine. For this you need the `gpg` tool, make sure it is installed on your machine.
In the [release page](https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest), download: On the [release page](https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest), download:
1. The release binary of your choice. 1. The release binary of your choice.
2. The `SHA256SUMS.asc` file 2. The `SHA256SUMS.asc` file
Then we will go through how to install Nicolas Dorier PGP keys on your system, and check the actual binaries.
### Importing Nicolas Dorier pgp keys (only first time) ### Importing Nicolas Dorier pgp keys (only first time)
This step should be done only one time. It makes sure your system knows Nicolas Dorier PGP Keys. This step should be done only one time. It ensures your system knows Nicolas Dorier's PGP keys.
Nicolas Dorier has a [keybase](https://keybase.io/NicolasDorier) account that allow you to verify that his identity is linked to several well-known social media accounts. Nicolas Dorier has a [keybase](https://keybase.io/NicolasDorier) account that allow you to verify that his identity is linked to several well-known social media accounts.
And as you can see on his profile page, the PGP key `62FE 8564 7DED DA2E` is linked to his keybase identity. And as you can see on his profile page, the PGP key `62FE 8564 7DED DA2E` is linked to his keybase identity.
@ -29,8 +27,15 @@ And as you can see on his profile page, the PGP key `62FE 8564 7DED DA2E` is lin
You can import this key from keybase: You can import this key from keybase:
```bash ```bash
curl https://keybase.io/nicolasdorier/pgp_keys.asc?fingerprint=7121bde3555d9be06bddc68162fe85647dedda2e | gpg --import curl https://keybase.io/nicolasdorier/pgp_keys.asc | gpg --import
``` ```
or
```bash
keybase pgp pull nicolasdorier
```
Alternatively, you can just download the file via the browser and run: Alternatively, you can just download the file via the browser and run:
```bash ```bash
@ -41,23 +46,44 @@ This step won't have to be repeated the next time you need to check a signature.
### Checking the actual PGP signature ### Checking the actual PGP signature
``` ```bash
sha256sum --check SHA256SUMS.asc sha256sum --check SHA256SUMS.asc --ignore-missing
``` ```
You should see that the file you downloaded has the right hash: You should see that the file you downloaded has the right hash:
```text
BTCPayServerVault-1.0.7-setup.exe: OK
``` ```
BTCPayServerVault-0.0.10-setup.exe: OK
If you are on Windows you can check the hashes are identical manually:
```powershell
certUtil -hashfile BTCPayServerVault-1.0.7-setup.exe SHA256
type SHA256SUMS.asc
```
If you are on macOS:
```bash
shasum -a 256 --check SHA256SUMS.asc
```
You should see that the file you downloaded has the right hash:
```text
BTCPayServerVault-osx-x64-1.0.7.dmg: OK
``` ```
Then check the actual signature: Then check the actual signature:
```bash ```bash
gpg --verify SHA256SUMS.asc gpg --verify SHA256SUMS.asc
``` ```
Which should output something like: Which should output something like:
``` ```text
gpg: Signature made Thu Dec 5 20:40:47 2019 JST gpg: Signature made Thu Dec 5 20:40:47 2019 JST
gpg: using RSA key 62FE85647DEDDA2E gpg: using RSA key 62FE85647DEDDA2E
gpg: Good signature from "BTCPayServer Vault <nicolas.dorier@gmail.com>" [unknown] gpg: Good signature from "BTCPayServer Vault <nicolas.dorier@gmail.com>" [unknown]