Compare commits

..

1 Commits

Author SHA1 Message Date
nicolas.dorier
1828bacfa8
Move from travis to github actions 2020-11-26 23:08:40 +09:00
70 changed files with 703 additions and 1478 deletions

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

@ -0,0 +1,59 @@
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@v2
- 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@v2
- 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@v2
- 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@v2
- 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 }}

View File

@ -1,51 +0,0 @@
---
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.-->

View File

@ -1,95 +0,0 @@
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 }}

53
.travis.yml Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
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,7 +5,6 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Helpers;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -13,7 +12,7 @@ namespace BTCPayServer.Hwi
{
public class HwiDeviceClient
{
public HwiDeviceClient(HwiClient hwiClient, DeviceSelector deviceSelector, string model, HDFingerprint? fingerprint)
public HwiDeviceClient(HwiClient hwiClient, DeviceSelector deviceSelector, HardwareWalletModels model, HDFingerprint? fingerprint)
{
HwiClient = hwiClient ?? throw new ArgumentNullException(nameof(hwiClient));
DeviceSelector = deviceSelector ?? throw new ArgumentNullException(nameof(deviceSelector));
@ -27,7 +26,7 @@ namespace BTCPayServer.Hwi
public string Password { get; set; }
public HwiClient HwiClient { get; }
public DeviceSelector DeviceSelector { get; }
public string Model { get; }
public HardwareWalletModels Model { get; }
public HDFingerprint? Fingerprint { get; }
public Task PromptPinAsync(CancellationToken cancellationToken = default)
@ -94,27 +93,21 @@ namespace BTCPayServer.Hwi
return NBitcoinHelpers.BetterParseExtPubKey(extPubKeyString, this.HwiClient.Network, HwiClient.IgnoreInvalidNetwork);
}
public async Task<BitcoinAddress> DisplayAddressAsync(ScriptPubKeyType addressType, KeyPath keyPath, CancellationToken cancellationToken = default)
public async Task DisplayAddressAsync(ScriptPubKeyType addressType, KeyPath keyPath, CancellationToken cancellationToken = default)
{
if (keyPath == null)
throw new ArgumentNullException(nameof(keyPath));
List<string> commandArguments = new List<string>();
commandArguments.Add("--path");
commandArguments.Add(keyPath.ToString(true, "h"));
commandArguments.Add("--addr-type");
switch (addressType)
{
case ScriptPubKeyType.Legacy:
commandArguments.Add("legacy");
break;
case ScriptPubKeyType.Segwit:
commandArguments.Add("wit");
commandArguments.Add("--wpkh");
break;
case ScriptPubKeyType.SegwitP2SH:
commandArguments.Add("sh_wit");
break;
case ScriptPubKeyType.TaprootBIP86:
commandArguments.Add("tap");
commandArguments.Add("--sh_wpkh");
break;
}
@ -123,55 +116,10 @@ namespace BTCPayServer.Hwi
commandArguments: commandArguments.ToArray(),
cancellationToken).ConfigureAwait(false);
return ParseAddress(response, HwiClient.Network, HwiClient.IgnoreInvalidNetwork);
if (!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)
{
if (psbt == null)

View File

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

View File

@ -15,6 +15,7 @@ namespace BTCPayServer.Hwi
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);
private HwiOption(HwiOptions type, string argument = null)

View File

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

View File

@ -1,301 +0,0 @@
// 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,10 +9,6 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NBitcoin.Logging;
using Microsoft.Extensions;
using BTCPayServer.Hwi.Process;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace BTCPayServer.Hwi.Transports
{
@ -33,6 +29,7 @@ namespace BTCPayServer.Hwi.Transports
public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel)
{
string responseString;
int exitCode;
var fileName = Path.Combine(hwiFolder, "hwi");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
!fileName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
@ -40,26 +37,25 @@ namespace BTCPayServer.Hwi.Transports
fileName += ".exe";
}
ProcessSpec processSpec = new ProcessSpec()
ProcessStartInfo startInfo = new ProcessStartInfo
{
Executable = fileName,
OutputCapture = new OutputCapture(),
FileName = fileName,
RedirectStandardOutput = true,
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
{
ProcessRunner processRunner = new ProcessRunner();
var exitCode = await processRunner.RunAsync(processSpec, cancel);
responseString = string.Concat(processSpec.OutputCapture.Lines);
process = await StartProcess(startInfo, cancel);
exitCode = process.ExitCode;
responseString = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
Logger.LogDebug($"Exit code: exit code: {exitCode}, Output: {responseString}");
}
catch (Exception ex)
@ -67,7 +63,37 @@ namespace BTCPayServer.Hwi.Transports
Logger.LogError(default, ex, "Failed to call hwi");
throw;
}
finally
{
try
{
if (!process.HasExited)
process.Kill();
}
catch { }
finally { process?.Dispose(); }
}
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

@ -1,50 +0,0 @@
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>
<PropertyGroup>
<Version>2.0.6</Version>
<Version>1.1.3</Version>
</PropertyGroup>
</Project>

View File

@ -1,15 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>12</LangVersion>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>8.0</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0-preview-20200116-01" />
<PackageReference Include="xunit" Version="2.4.1" />
<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>

View File

@ -23,7 +23,7 @@ namespace BTCPayServer.Vault.Tests
private ILogger _logger;
private ILogger _HwiLogger;
public HwiTester(Network network, ILoggerFactory loggerFactory, string hwiPath)
public HwiTester(ILoggerFactory loggerFactory, string hwiPath)
{
if (hwiPath == null)
throw new ArgumentNullException(nameof(hwiPath));
@ -31,21 +31,20 @@ namespace BTCPayServer.Vault.Tests
throw new ArgumentNullException(nameof(loggerFactory));
_logger = loggerFactory.CreateLogger("HwiTester");
_HwiLogger = loggerFactory.CreateLogger("CliTransport");
Network = network;
Client = new HwiClient(network)
Client = new HwiClient(Network)
{
IgnoreInvalidNetwork = true,
Transport = new LegacyCompatibilityTransport(new CliTransport(Path.GetDirectoryName(hwiPath))
Transport = new CliTransport(hwiPath)
{
Logger = _HwiLogger
})
}
};
}
public static async Task<HwiTester> CreateAsync(Network network, ILoggerFactory loggerFactory)
public static async Task<HwiTester> CreateAsync(ILoggerFactory loggerFactory)
{
var hwi = await HwiVersions.Latest.Current.EnsureIsDeployed();
return new HwiTester(network, loggerFactory, hwi);
return new HwiTester(loggerFactory, hwi);
}
public async Task EnsureHasDevice()
@ -55,7 +54,7 @@ namespace BTCPayServer.Vault.Tests
throw new InvalidOperationException("No device supported by HWI has been plugged");
}
public Network Network { get; }
public Network Network => NBitcoin.Network.RegTest;
public HwiClient Client
{
@ -67,21 +66,5 @@ namespace BTCPayServer.Vault.Tests
get;
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,12 +5,15 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using Xunit;
using Xunit.Abstractions;
using BTCPayServer.Hwi;
using BTCPayServer.Hwi.Transports;
using System.Collections.Generic;
using System.Net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Builder;
using BTCPayServer.Vault;
namespace BTCPayServer.Vault.Tests
{
@ -26,26 +29,6 @@ namespace BTCPayServer.Vault.Tests
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]
public async Task CanGetVersion()
{
@ -93,7 +76,18 @@ namespace BTCPayServer.Vault.Tests
public async Task CanGetXPub()
{
var tester = await CreateTester();
await tester.Device.GetXPubAsync(new KeyPath("44'/0'/0'/0/0"));
await tester.Device.GetXPubAsync(new KeyPath("1'"));
}
[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]
@ -101,11 +95,11 @@ namespace BTCPayServer.Vault.Tests
public async Task CanDisplayAddress()
{
var tester = await CreateTester();
await tester.Device.DisplayAddressAsync(ScriptPubKeyType.Legacy, tester.GetKeyPath(ScriptPubKeyType.Legacy).Derive("0/1"));
await tester.Device.DisplayAddressAsync(ScriptPubKeyType.Legacy, GetKeyPath(ScriptPubKeyType.Legacy).Derive("0/1"));
if (tester.Network.Consensus.SupportSegwit)
{
await tester.Device.DisplayAddressAsync(ScriptPubKeyType.Segwit, tester.GetKeyPath(ScriptPubKeyType.Segwit).Derive("0/1"));
await tester.Device.DisplayAddressAsync(ScriptPubKeyType.SegwitP2SH, tester.GetKeyPath(ScriptPubKeyType.SegwitP2SH).Derive("0/1"));
await tester.Device.DisplayAddressAsync(ScriptPubKeyType.Segwit, GetKeyPath(ScriptPubKeyType.Segwit).Derive("0/1"));
await tester.Device.DisplayAddressAsync(ScriptPubKeyType.SegwitP2SH, GetKeyPath(ScriptPubKeyType.SegwitP2SH).Derive("0/1"));
}
}
@ -114,6 +108,7 @@ namespace BTCPayServer.Vault.Tests
public async Task CanSign()
{
var tester = await CreateTester();
// Should show we are sending 2.0 BTC three time
var psbt = await tester.Device.SignPSBTAsync(await CreatePSBT(tester, ScriptPubKeyType.Legacy));
AssertFullySigned(tester, psbt);
@ -136,7 +131,7 @@ namespace BTCPayServer.Vault.Tests
private async Task<PSBT> CreatePSBT(HwiTester tester, ScriptPubKeyType addressType)
{
var accountKeyPath = new RootedKeyPath(tester.Device.Fingerprint.Value, tester.GetKeyPath(addressType));
var accountKeyPath = new RootedKeyPath(tester.Device.Fingerprint.Value, GetKeyPath(addressType));
var accountKey = await tester.Device.GetXPubAsync(accountKeyPath.KeyPath);
Logger.LogInformation($"Signing with xpub {accountKeyPath}: {accountKey}...");
List<Transaction> knownTransactions = new List<Transaction>();
@ -153,13 +148,28 @@ namespace BTCPayServer.Vault.Tests
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)
{
var pubkey = xpub.Derive(new KeyPath(path)).ExtPubKey.PubKey;
if (addressType == ScriptPubKeyType.Legacy)
{
var prevTx = xpub.Network.CreateTransaction();
prevTx.Inputs.Add(RandomOutpoint(), ((IDestination)new Key()).ScriptPubKey);
prevTx.Inputs.Add(RandomOutpoint(), new Key().ScriptPubKey);
var txout = prevTx.Outputs.Add(money, pubkey.GetScriptPubKey(addressType));
var coin = new Coin(new OutPoint(prevTx, 0), txout);
builder.AddCoins(coin);
@ -189,13 +199,10 @@ namespace BTCPayServer.Vault.Tests
{
return new OutPoint(RandomUtils.GetUInt256(), 0);
}
Task<HwiTester> CreateTester(bool needDevice = true)
async Task<HwiTester> CreateTester(bool needDevice = true)
{
return CreateTester(Network.Main, needDevice);
}
async Task<HwiTester> CreateTester(Network network, bool needDevice = true)
{
var tester = await HwiTester.CreateAsync(network, LoggerFactory);
var tester = await HwiTester.CreateAsync(LoggerFactory);
if (needDevice)
await tester.EnsureHasDevice();
return tester;

View File

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

View File

@ -1,7 +1,7 @@

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,12 +10,13 @@ using Avalonia.Controls;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
using AvalonStudio.Shell;
using AvalonStudio.Shell.Extensibility.Platforms;
using Microsoft.Extensions.DependencyInjection;
using Avalonia.Controls.Platform;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using System.Threading;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Vault
{
@ -23,6 +24,8 @@ namespace BTCPayServer.Vault
{
public static void AddAvalonia<TApp>(this IServiceCollection services) where TApp : Application, new()
{
bool useGpuLinux = true;
var result = AppBuilder.Configure<TApp>();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@ -31,17 +34,36 @@ namespace BTCPayServer.Vault
.UseWin32()
.UseSkia();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
if (DetectLLVMPipeRasterizer())
{
useGpuLinux = false;
}
result.UsePlatformDetect();
}
else
{
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();
result = result
.With(new Win32PlatformOptions())
.With(new X11PlatformOptions { WmClass = title })
.With(new AvaloniaNativePlatformOptions())
.With(new Win32PlatformOptions { AllowEglInitialization = true, UseDeferredRendering = true })
.With(new X11PlatformOptions { UseGpu = useGpuLinux, WmClass = title })
.With(new AvaloniaNativePlatformOptions { UseDeferredRendering = true, UseGpu = true })
.With(new MacOSPlatformOptions { ShowInDock = true });
services.AddSingleton(result);
services.AddSingleton<MainWindow>();
@ -99,56 +121,5 @@ namespace BTCPayServer.Vault
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,19 +37,8 @@ namespace BTCPayServer.Vault.HWI
internal async Task Handle(HttpContext ctx)
{
if (!ctx.Request.Headers.TryGetValue("Origin", out var origin))
if (ctx.Request.Path.Value == "")
{
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))
{
ctx.Response.StatusCode = 400;
@ -63,19 +52,23 @@ namespace BTCPayServer.Vault.HWI
}
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))
{
ctx.Response.StatusCode = 429;
return;
}
if (await _permissionsService.IsGranted(originReason))
if (await _permissionsService.IsGranted(origin))
{
ctx.Response.StatusCode = 200;
return;
}
if (!await _permissionPrompt.AskPermission(originReason, ctx.RequestAborted))
if (!await _permissionPrompt.AskPermission(origin, ctx.RequestAborted))
{
_logger.LogInformation($"Permission to {origin} got denied");
ctx.Response.StatusCode = 401;

View File

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

View File

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

View File

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

View File

@ -3,8 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
SizeToContent="Height"
CanResize="false"
SizeToContent="Manual"
Width="622"
Height="433"
Icon="avares://BTCPayServer.Vault/Assets/BTCPayServerVault-256x256.png"
@ -18,15 +17,15 @@
<DockPanel>
<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">
<Image Source="{DynamicResource spinner}"/>
<DrawingPresenter Drawing="{DynamicResource spinner}"></DrawingPresenter>
<TextBlock VerticalAlignment="Center" Margin="5,0,0,0" Text="{Binding CurrentOperation}"></TextBlock>
</StackPanel>
</DockPanel>
<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>
<DockPanel>
<Image DockPanel.Dock="Left" Height="128" Width="128" VerticalAlignment="Center" HorizontalAlignment="Center" Source="{DynamicResource warning}" />
<StackPanel IsVisible="{Binding HWIVisible}" Margin="10,0,0,0" VerticalAlignment="Center">
<DrawingPresenter DockPanel.Dock="Left" Height="128" Width="128" VerticalAlignment="Center" HorizontalAlignment="Center" Drawing="{DynamicResource warning}" />
<StackPanel Margin="10,0,0,0" VerticalAlignment="Center">
<TextBlock>A website is requesting access to your hardware wallets.</TextBlock>
<TextBlock>If you accept, the website will be able to:</TextBlock>
<TextBlock Text=" "></TextBlock>
@ -65,45 +64,6 @@
</Button>
</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>
</StackPanel>
</StackPanel>

View File

@ -16,6 +16,26 @@ namespace BTCPayServer.Vault
{
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()
{
@ -23,11 +43,12 @@ namespace BTCPayServer.Vault
Title = Extensions.GetTitle();
}
private DispatcherTimer _BlinkTimer;
DispatcherTimer _ResizeHackTimer;
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
if (Program.CurrentServiceProvider is { } serviceProvider)
Context = AvaloniaSynchronizationContext.Current as AvaloniaSynchronizationContext;
if (AvaloniaLocator.CurrentMutable?.GetService<IServiceProvider>() is IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Indicator = ServiceProvider.GetRequiredService<IRunningIndicator>();
@ -35,12 +56,14 @@ namespace BTCPayServer.Vault
Indicator.StoppedRunning += OnStoppedRunning;
DataContext = ServiceProvider.GetRequiredService<MainWindowViewModel>();
MainViewModel.PropertyChanged += MainViewModel_PropertyChanged;
_BlinkTimer = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Normal, (_, __) =>
_ResizeHackTimer = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Normal, (_, __) =>
{
ResizeHack();
if (MainViewModel.IsVisible && this.WindowState == WindowState.Minimized)
this.Blink();
});
_BlinkTimer.Start();
_ResizeHackTimer.Start();
this.ResizeHack();
}
}
private void MainViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
@ -49,12 +72,13 @@ namespace BTCPayServer.Vault
{
Context.Post(_ =>
{
this.ResizeHack();
this.ActivateHack();
}, null);
}
}
protected override void OnClosing(WindowClosingEventArgs e)
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
if (Indicator != null)
@ -62,7 +86,7 @@ namespace BTCPayServer.Vault
Indicator.Running -= OnRunning;
Indicator.StoppedRunning -= OnStoppedRunning;
MainViewModel.PropertyChanged -= MainViewModel_PropertyChanged;
_BlinkTimer.Stop();
_ResizeHackTimer.Stop();
}
}
@ -93,14 +117,14 @@ namespace BTCPayServer.Vault
public IServiceProvider ServiceProvider { get; private set; }
public IRunningIndicator Indicator { get; private set; }
AvaloniaSynchronizationContext Context = new AvaloniaSynchronizationContext();
AvaloniaSynchronizationContext Context;
internal async Task<bool> Authorize(OriginReason originReason)
internal async Task<bool> Authorize(string origin)
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Context.Post((state) =>
{
MainViewModel.Authorize(originReason, tcs);
MainViewModel.Authorize(origin, tcs);
}, null);
return await tcs.Task;
}

View File

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

View File

@ -1,159 +0,0 @@
#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

@ -1,36 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@ -3,16 +3,16 @@
<Style>
<Style.Resources>
<SolidColorBrush x:Key="btcpay-color-primary">#329f80</SolidColorBrush>
<DrawingImage x:Key="warning">
<DrawingGroup>
<DrawingGroup x:Key="warning">
<DrawingGroup.Children>
<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="#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" />
</DrawingGroup>
</DrawingImage>
<DrawingImage x:Key="spinner">
<DrawingGroup>
</DrawingGroup.Children>
</DrawingGroup>
<DrawingGroup x:Key="spinner">
<DrawingGroup.Children>
<GeometryDrawing Brush="#329f80">
<GeometryDrawing.Geometry>
<EllipseGeometry Rect="0 0 20 20"></EllipseGeometry>
@ -23,8 +23,8 @@
<EllipseGeometry Rect="2.5 2.5 15 15"></EllipseGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage>
</DrawingGroup.Children>
</DrawingGroup>
</Style.Resources>
</Style>
<Style Selector="TextBlock">

View File

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

9
Build/CI/applesign.md Executable file → Normal 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.
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).
You will also need to set `APPLE_ID` and `APPLE_ID_PASSWORD`, using [app-specific password](https://support.apple.com/en-us/HT204397).
## How to
If you are on linux and try to sign with an apple certificate you need to do the following steps:
@ -28,7 +28,6 @@ Let's create it:
```bash
rsa_key_file="temp.key"
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"
```
This will create a request file as `request.csr` in your current folder.
@ -44,7 +43,7 @@ cer_file="developerID_application.cer"
pem_file="developerID_application.pem"
cert_output_file="developerID_application.p12"
openssl x509 -in "$cer_file" -inform DER -out "$pem_file" -outform PEM
openssl pkcs12 -export -inkey "$rsa_key_file" -in "$pem_file" -legacy -out "$cert_output_file"
openssl pkcs12 -export -inkey "$rsa_key_file" -in "$pem_file" -out "$cert_output_file"
```
Now enter a password, don't pick an empty one as the rest would fail.
@ -61,4 +60,8 @@ For travis to sign BTCPayServer.Vault dmg file, you need to convert the certific
```bash
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?

29
Build/CI/applesign.sh Executable file → Normal file
View File

@ -42,7 +42,7 @@ echo "Starting apple signing..."
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')"
AZURE_ACCOUNT_NAME="$(echo "$AZURE_STORAGE_CONNECTION_STRING" | cut -d'=' -f3 | cut -d';' -f1)"
DIRECTORY_NAME="dist-$GITHUB_RUN_ID"
DIRECTORY_NAME="dist-$TRAVIS_BUILD_ID"
RUNTIME="osx-x64"
tar_file="BTCPayServerVault-${RUNTIME}-$version.tar.gz"
@ -94,12 +94,35 @@ echo "DMG signed"
echo "Notarize $dmg_file with bundle id $bundle_id"
sudo xcrun notarytool submit --apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_TEAM_ID" --wait "$dmg_file"
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
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"
echo "Installing az..."
export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1
brew update
brew install azure-cli || true
BLOB_NAME="$DIRECTORY_NAME/$dmg_file"

18
Build/CI/build.sh Executable file → Normal file
View File

@ -14,18 +14,12 @@ 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
if [[ "$GITHUB_REF_NAME" ]]; then
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
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
@ -37,7 +31,7 @@ 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)"
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")"

39
Build/CI/makerelease.sh Executable file → Normal file
View File

@ -6,43 +6,38 @@ if ! [[ "$AZURE_STORAGE_CONNECTION_STRING" ]] || ! [[ "$AZURE_STORAGE_CONTAINER"
exit 0
fi
if ! [[ "$GITHUB_REF" ]]; then
echo "Skipping github release (GITHUB_REF is not set)"
if ! [[ "$GITHUB_TOKEN" ]]; then
echo "Skipping github release (GITHUB_TOKEN is not set)"
exit 0
fi
# GITHUB_REF= refs/tags/Vault/v1.0.6-test
GITHUB_REF_NAME="$(echo $GITHUB_REF | cut -d'/' -f4)"
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
if ! [[ "$TRAVIS_TAG" ]]; then
echo "Skipping github release (TRAVIS_TAG is not set)"
exit 0
fi
AZURE_ACCOUNT_NAME="$(echo "$AZURE_STORAGE_CONNECTION_STRING" | cut -d'=' -f3 | cut -d';' -f1)"
DIRECTORY_NAME="dist-$GITHUB_RUN_ID"
DIRECTORY_NAME="dist-$TRAVIS_BUILD_ID"
wget -O azcopy.tar.gz https://aka.ms/downloadazcopy-v10-linux
tar -xf azcopy.tar.gz --strip-components=1
mkdir -p dist
./azcopy cp "https://$AZURE_ACCOUNT_NAME.blob.core.windows.net/$AZURE_STORAGE_CONTAINER/$DIRECTORY_NAME/*" "dist"
# Our container is public, so the SAS token should not be needed
# 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)"
version="$(echo "$GITHUB_REF_NAME" | cut -d'/' -f2)"
payload="$(jq -M --arg "tag_name" "$GITHUB_REF_NAME" \
version="$(echo "$TRAVIS_TAG" | cut -d'/' -f2)"
payload="$(jq -M --arg "tag_name" "$TRAVIS_TAG" \
--arg "name" "BTCPayServer Vault $version" \
--arg "body" "$release" \
--argjson "draft" $draft \
--argjson "prerelease" $prerelease \
--argjson "draft" false \
--argjson "prerelease" true \
'. | .tag_name=$tag_name | .name=$name | .body=$body | .draft=$draft | .prerelease=$prerelease' \
<<<'{}')"
echo "Creating release to https://api.github.com/repos/$GITHUB_REPOSITORY/releases"
echo "Creating release to https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases"
echo "$payload"
response="$(curl --fail -s -S -X POST https://api.github.com/repos/$GITHUB_REPOSITORY/releases \
response="$(curl --fail -s -S -X POST https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
@ -66,5 +61,5 @@ for f in *; do
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: $media_type" \
--data-binary @"$f" \
"https://uploads.github.com/repos/$GITHUB_REPOSITORY/releases/$release_id/assets?name=$f"
"https://uploads.github.com/repos/$TRAVIS_REPO_SLUG/releases/$release_id/assets?name=$f"
done

13
Build/CI/pgpsign.sh Executable file → Normal file
View File

@ -11,12 +11,14 @@ if ! [[ "$PGP_KEY" ]]; then
fi
AZURE_ACCOUNT_NAME="$(echo "$AZURE_STORAGE_CONNECTION_STRING" | cut -d'=' -f3 | cut -d';' -f1)"
DIRECTORY_NAME="dist-$GITHUB_RUN_ID"
DIRECTORY_NAME="dist-$TRAVIS_BUILD_ID"
wget -O azcopy.tar.gz https://aka.ms/downloadazcopy-v10-linux
tar -xf azcopy.tar.gz --strip-components=1
mkdir -p dist
./azcopy cp "https://$AZURE_ACCOUNT_NAME.blob.core.windows.net/$AZURE_STORAGE_CONTAINER/$DIRECTORY_NAME/*" "dist"
# Our container is public, so the SAS token should not be needed
# 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
for f in *; do
if [[ "$f" == "SHA256SUMS" ]]; then continue; fi
@ -25,8 +27,7 @@ done
mv /tmp/SHA256SUMS SHA256SUMS
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
echo "$PGP_KEY" | base64 --decode | gpg --import --no-tty
echo "PGP keys correctly imported"
gpg --no-tty --digest-algo sha256 --clearsign SHA256SUMS
echo "$PGP_KEY" | base64 --decode | gpg --import
gpg --digest-algo sha256 --clearsign SHA256SUMS
az storage blob upload -f "SHA256SUMS.asc" -c "$AZURE_STORAGE_CONTAINER" -n "$DIRECTORY_NAME/SHA256SUMS.asc"
rm SHA256SUMS

0
Build/CI/selfsignedcert.sh Executable file → Normal file
View File

View File

@ -9,10 +9,6 @@ The process to publish a new version is the following:
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.
## 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
The debian package is easy to test, run:

View File

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

View File

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

View File

@ -7,6 +7,5 @@ Vcs-Git: git://github.com/btcpayserver/BTCPayServer.Vault.git
Vcs-Browser: https://github.com/btcpayserver/BTCPayServer.Vault
Architecture: amd64
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}
Description: {DESCRIPTION}

View File

@ -0,0 +1,9 @@
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

@ -1,11 +1,3 @@
# 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:
SUBSYSTEMS=="usb", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666"
@ -13,3 +5,4 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plu
# required:
# from <https://github.com/signal11/hidapi/blob/master/udev/99-hid.rules>
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/
#
# 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
# 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"
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}=="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
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,7 +0,0 @@
[Desktop Entry]
Type=Application
Name=BTCPayServer Vault
Exec=/usr/local/bin/BTCPayServer.Vault
Icon=BTCPayServerVault
Categories=Utility;

View File

@ -1,18 +1,13 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder
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 && \
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 && \
tar -zxvf /tmp/hwi.tar.gz -C /tmp hwi && \
echo "d9cc65de95e3cf93fd3c953d589184a00180624ffc5ad17aade97616a8919fa6 /tmp/hwi" | sha256sum -c - && \
echo "23ea301117f74561294b5b3ebe1eeb461004aff7e479c4b90a0aaec5924cc677 /tmp/hwi" | sha256sum -c - && \
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
ENV RUNTIME "linux-x64"
COPY "Build/common" "Build/common"
COPY "Build/linux-x64" "Build/linux-x64"
ENV EXPORT_VARIABLES "source Build/common/export-variables.sh"
COPY BTCPayServer.Vault/BTCPayServer.Vault.csproj BTCPayServer.Vault/BTCPayServer.Vault.csproj
COPY BTCPayServer.Hwi/BTCPayServer.Hwi.csproj BTCPayServer.Hwi/BTCPayServer.Hwi.csproj
@ -23,17 +18,10 @@ COPY BTCPayServer.Hwi BTCPayServer.Hwi
COPY BTCPayServer.Vault BTCPayServer.Vault
RUN $EXPORT_VARIABLES && dotnet_publish && mv /tmp/hwi "$PUBLISH_FOLDER/"
COPY "BTCPayServerVault.png" "BTCPayServerVault.png"
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 \( -name 'hwi' -o -name "$EXECUTABLE" -o -name '*.sh' \) -exec chmod +x {} \; && \
find "$PUBLISH_FOLDER" -type f \( -name 'hwi' -o -name "$EXECUTABLE" \) -exec chmod +x {} \; && \
# 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" *
ENTRYPOINT [ "/bin/bash", "-c", "$EXPORT_VARIABLES && cp -a $DIST/* /opt/dist/" ]
ENTRYPOINT [ "/bin/bash", "-c", "$EXPORT_VARIABLES && cp $DIST/* /opt/dist/" ]

View File

@ -1,28 +0,0 @@
#!/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

@ -1,12 +0,0 @@
# 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,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder
# Optimize docker cache, do not make it one layer
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 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 && \
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 && \
tar -zxvf /tmp/hwi.tar.gz -C /tmp hwi && \
echo "b3764a530b635e7a7348c9185e09e74b389f5f585094fe316f700eec7c761875 /tmp/hwi" | sha256sum -c - && \
echo "dc516e563db7c0f21b3f017313fc93a2a57f8d614822b8c71f1467a4e5f59dbb /tmp/hwi" | sha256sum -c - && \
rm /tmp/hwi.tar.gz
WORKDIR /source

View File

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

View File

@ -1,11 +0,0 @@
#!/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,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder
# Optimize docker cache, do not make it one layer
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 wget -qO "/tmp/hwi.zip" https://github.com/bitcoin-core/HWI/releases/download/3.2.0/hwi-3.2.0-windows-x86_64.zip && \
RUN wget -qO "/tmp/hwi.zip" https://github.com/bitcoin-core/HWI/releases/download/1.2.1/hwi-1.2.1-windows-amd64.zip && \
unzip "/tmp/hwi.zip" -d "/tmp" && \
echo "e068d91b664597425a8ead02d7b86a02ad6c4b72746c42961f58a58b08f9fd79 /tmp/hwi.exe" | sha256sum -c - && \
echo "b8b21499592a311cfaa18676280807d6bf674d72cef21409ed265069f6582c1b /tmp/hwi.exe" | sha256sum -c - && \
rm "/tmp/hwi.zip" && \
# Need to setup with rcedit because https://github.com/dotnet/sdk/issues/3943
# I prebuild the binaries with VS 2019 on commit b807b34a644c86c0b0d89c7f073967e79202731a
@ -39,6 +39,14 @@ RUN $EXPORT_VARIABLES && \
done && \
convert /tmp/BTCPayServerVault.ico.tmp/*.png /tmp/BTCPayServerVault.ico && \
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" \
--set-icon "/tmp/BTCPayServerVault.ico" \
--set-version-string "LegalCopyright" "$LICENSE" \

View File

@ -1,68 +1,16 @@
[![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)
[![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)
# BTCPayServer.Vault
This project is composed of two parts:
* [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](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/)
* [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.Vault](BTCPayServer.Vault): A simple local web server providing access to the hardware wallet physically connected to your computer via hwi.
## Why BTCPayServer Vault
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
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.
@ -97,7 +45,7 @@ This is why BTCPayServer Vault always ask permission to user first before allowi
This is a two step process:
1. Install the latest version of the [.NET Core 6.0 SDK](https://dotnet.microsoft.com/download/dotnet-core/6.0)
1. Install the latest version of the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)
2. Run `dotnet build`
If you want to run it for testing:
@ -111,10 +59,10 @@ dotnet run
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)
[![](https://img.youtube.com/vi/hh_cm8MKl2g/mqdefault.jpg)](https://www.youtube.com/watch?v=hh_cm8MKl2g)
- [BTCPay Vault announcement](https://blog.btcpayserver.org/btcpay-vault/)
- [Using BTCPay Vault with BTCPay Server](https://docs.btcpayserver.org/Vault)
- [Using BTCPay Vault with BTCPay Server](https://docs.btcpayserver.org/features/vault)
## Licence

View File

@ -1,25 +1,27 @@
# How to verify release signatures
# How to verify release binaries signatures
## Introduction
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.
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.
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.
## Checking PGP signatures<a name="pgp"></a>
## Cheking PGP signatures<a name="pgp"></a>
For this you need the `gpg` tool, make sure it is installed on your machine.
On the [release page](https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest), download:
In the [release page](https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest), download:
1. The release binary of your choice.
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)
This step should be done only one time. It ensures your system knows Nicolas Dorier's PGP keys.
This step should be done only one time. It makes sure your system knows Nicolas Dorier 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.
And as you can see on his profile page, the PGP key `62FE 8564 7DED DA2E` is linked to his keybase identity.
@ -27,15 +29,8 @@ 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:
```bash
curl https://keybase.io/nicolasdorier/pgp_keys.asc | gpg --import
curl https://keybase.io/nicolasdorier/pgp_keys.asc?fingerprint=7121bde3555d9be06bddc68162fe85647dedda2e | gpg --import
```
or
```bash
keybase pgp pull nicolasdorier
```
Alternatively, you can just download the file via the browser and run:
```bash
@ -46,44 +41,23 @@ This step won't have to be repeated the next time you need to check a signature.
### Checking the actual PGP signature
```bash
sha256sum --check SHA256SUMS.asc --ignore-missing
```
sha256sum --check SHA256SUMS.asc
```
You should see that the file you downloaded has the right hash:
```text
BTCPayServerVault-1.0.7-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
BTCPayServerVault-0.0.10-setup.exe: OK
```
Then check the actual signature:
```bash
gpg --verify SHA256SUMS.asc
```
Which should output something like:
```text
```
gpg: Signature made Thu Dec 5 20:40:47 2019 JST
gpg: using RSA key 62FE85647DEDDA2E
gpg: Good signature from "BTCPayServer Vault <nicolas.dorier@gmail.com>" [unknown]