Compare commits
1 Commits
main
...
ryan/optim
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | af230f55df |
|
@ -1,2 +0,0 @@
|
|||
[registries.distrust]
|
||||
index = "https://git.distrust.co/public/_cargo-index.git"
|
277
CHANGELOG.md
277
CHANGELOG.md
|
@ -1,277 +0,0 @@
|
|||
# Keyfork v0.2.4
|
||||
|
||||
This release includes a lot of "maintenance" changes, without any changes in
|
||||
end-user functionality.
|
||||
|
||||
### Changes in keyfork:
|
||||
|
||||
The most significant change in this release is the reorganization of some of
|
||||
the subcommands, where they would be better as enum-traits, such as `keyfork
|
||||
derive` and `keyfork wizard`.
|
||||
|
||||
```
|
||||
b254ba7 cleanup post-merge
|
||||
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
|
||||
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
|
||||
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
|
||||
5219c5a keyfork: enum-trait-ify choose-your-own commands
|
||||
b26f296 keyfork-derive-path-data: move all pathcrafting here
|
||||
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
|
||||
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
|
||||
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
|
||||
```
|
||||
|
||||
### Changes in keyfork-derive-openpgp:
|
||||
|
||||
```
|
||||
b254ba7 cleanup post-merge
|
||||
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
|
||||
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
|
||||
b26f296 keyfork-derive-path-data: move all pathcrafting here
|
||||
```
|
||||
|
||||
### Changes in keyfork-derive-path-data:
|
||||
|
||||
This change now centralizes all special Keyfork paths. This means crates should
|
||||
no longer be required to implement their own path parsing logic.
|
||||
|
||||
```
|
||||
b26f296 keyfork-derive-path-data: move all pathcrafting here
|
||||
```
|
||||
|
||||
### Changes in keyfork-derive-util:
|
||||
|
||||
```
|
||||
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
|
||||
```
|
||||
|
||||
### Changes in keyfork-mnemonic:
|
||||
|
||||
`keyfork-mnemonic-util` has finally been renamed to `keyfork-mnemonic`. The
|
||||
method names `as_bytes() => as_slice()`, `to_bytes() => to_vec()`, and
|
||||
`into_bytes() => into_vec()`, and the function names
|
||||
`from_bytes() => try_from_slice()` and
|
||||
`from_nonstandard_bytes() => from_array()`, have been implemented to more
|
||||
closely represent the native types they are representing. Additionally,
|
||||
`Mnemonic::generate_seed()` has been modified to return a constant size array;
|
||||
this is a breaking change, but should have minimal impact.
|
||||
|
||||
```
|
||||
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
|
||||
3ee81b6 keyfork-mnemonic-util: impl as_slice to_vec into_vec
|
||||
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
|
||||
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
|
||||
```
|
||||
|
||||
### Changes in keyfork-prompt:
|
||||
|
||||
```
|
||||
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
|
||||
```
|
||||
|
||||
### Changes in keyfork-shard:
|
||||
|
||||
```
|
||||
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
|
||||
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
|
||||
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
|
||||
```
|
||||
|
||||
### Changes in keyforkd:
|
||||
|
||||
```
|
||||
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
|
||||
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
|
||||
536e6da keyforkd{,-client}: lots of documentationings
|
||||
```
|
||||
|
||||
### Changes in keyforkd-client:
|
||||
|
||||
```
|
||||
536e6da keyforkd{,-client}: lots of documentationings
|
||||
```
|
||||
|
||||
# Keyfork v0.2.3
|
||||
|
||||
This release includes a bugfix for the wizard where the wizard was too strict
|
||||
about when keys were "alive".
|
||||
|
||||
### Changes in keyfork:
|
||||
|
||||
```
|
||||
dd4354f keyfork: bump keyfork-shard
|
||||
```
|
||||
|
||||
### Changes in keyfork-shard:
|
||||
|
||||
```
|
||||
ba64db8 update Cargo.toml and Cargo.lock
|
||||
fa84a2a keyfork-shard: Be less strict about keys
|
||||
```
|
||||
|
||||
# Keyfork v0.2.2
|
||||
|
||||
This release adds a new wizard, intended to be used at DEFCON 32.
|
||||
|
||||
### Changes in keyfork:
|
||||
|
||||
```
|
||||
8d40d26 keyfork: add `bottoms-up` wizard
|
||||
```
|
||||
|
||||
### Changes in keyfork-derive-openpgp:
|
||||
|
||||
This change also includes a minor change, allowing the derivation path for
|
||||
`keyfork-derive-openpg` to derive further than two paths, which was useful in
|
||||
the testing of the wizard.
|
||||
|
||||
```
|
||||
8d40d26 keyfork: add `bottoms-up` wizard
|
||||
```
|
||||
|
||||
# Keyfork v0.2.1
|
||||
|
||||
This release contains an emergency bugfix for Keyfork Shard, which previously
|
||||
would not be able to properly verify the length of remote shard QR codes.
|
||||
|
||||
# Keyfork v0.2.0
|
||||
|
||||
Some of the changes in this release are based on feedback from audits
|
||||
(publications coming soon!). The previous version of Keyfork, in almost every
|
||||
configuration, is safe to use. The most significant change in this version
|
||||
affects Keyfork Shard, which has an incompatible difference between this
|
||||
version and the previous version. Information about shards, such as the length
|
||||
of the shard, could be leaked and discovered by an attacker when using the
|
||||
Remote Shard recovery mechanism.
|
||||
|
||||
An additional change is the requirement of hardened indices on the first two
|
||||
levels of key derivation. This is due to Keyfork potentially leaking private
|
||||
keys when hardened derivation is not used. To be completely honest, I don't
|
||||
entirely understand the math behind it.
|
||||
|
||||
There is no reason to upgrade if Keyfork has been used as-is, as all supported
|
||||
provisioners at this point in time require hardened derivation at all steps.
|
||||
|
||||
### Changes in keyfork:
|
||||
|
||||
```
|
||||
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
|
||||
5d2309e keyfork-prompt: add SecurePinValidator for making new, secure, PINs
|
||||
cdf4015 keyfork wizard: use correct derivation path for re-deriving shard decryption keys
|
||||
f0e5ae9 keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE
|
||||
289cec3 keyfork wizard: upcast i and index to avoid wrapping add
|
||||
9394500 keyfork-shard: generate nonce using hkdf
|
||||
```
|
||||
|
||||
### Changes in keyfork-derive-openpgp:
|
||||
|
||||
```
|
||||
f0e5ae9 keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE
|
||||
9f089e7 keyfork-derive-openpgp: use .first() in place of .get(0)
|
||||
```
|
||||
|
||||
### Changes in keyfork-derive-util:
|
||||
|
||||
```
|
||||
de4e98a keyfork-derive-util: black-box checking all zeroes
|
||||
48ccd7c keyfork-derive-util: add note about potential side-channel when verifying keys
|
||||
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
|
||||
1de466c keyfork-derive-util: allow zeroable input for non-master-key derivation
|
||||
61871a7 keyfork-derive-util: make private and public test keys more visible
|
||||
2bca0a1 keyfork-derive-util: make Test{Public,Private}Key public, rename Internal algorithm
|
||||
```
|
||||
|
||||
### Changes in keyfork-entropy:
|
||||
|
||||
```
|
||||
5438f4e keyfork-entropy: downgrade entropy size limit to warning
|
||||
```
|
||||
|
||||
### Changes in keyfork-mnemonic-util:
|
||||
|
||||
```
|
||||
001fc0b remove trailing hitespace :(
|
||||
6a265ad keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes
|
||||
```
|
||||
|
||||
### Changes in keyfork-prompt:
|
||||
|
||||
```
|
||||
5d2309e keyfork-prompt: add SecurePinValidator for making new, secure, PINs
|
||||
```
|
||||
|
||||
### Changes in keyfork-qrcode:
|
||||
|
||||
```
|
||||
fa125e7 keyfork-qrcode: prefer Instant over SystemTime for infallible time comparison
|
||||
```
|
||||
|
||||
### Changes in keyfork-shard:
|
||||
|
||||
```
|
||||
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
|
||||
1a036a0 keyfork-shard: clean up documentation for encrypted shard padding
|
||||
e068743 keyfork-shard: display error message on duplicate key fingerprints found
|
||||
23db509 keyfork-shard: improve wording for counting shardholders
|
||||
9461772 keyfork-shard: ignore duplicate certificate entries
|
||||
6a265ad keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes
|
||||
c0b19e2 keyfork-shard: assert shared secrets are contributory
|
||||
0fe5301 keyfork-shard: add in bug messages
|
||||
08a66e2 keyfork-shard: base64 encode content instead of base16
|
||||
6fa434e keyfork-shard: shorten length and pad inside encrypted block
|
||||
9394500 keyfork-shard: generate nonce using hkdf
|
||||
194d475 keyfork-shard: validate signatures using shard-specific validation requirements
|
||||
```
|
||||
|
||||
### Changes in keyfork-zbar:
|
||||
|
||||
```
|
||||
0c76869 .cargo/config.toml: add registry configuration :)
|
||||
```
|
||||
|
||||
### Changes in keyforkd:
|
||||
|
||||
```
|
||||
bcfcc87 keyforkd: add warning when loading seed with less than 128 bits
|
||||
40551a5 keyforkd: require hardened derivation on two highest indexes
|
||||
```
|
||||
|
||||
### Changes in keyforkd-client:
|
||||
|
||||
```
|
||||
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
|
||||
1de466c keyfork-derive-util: allow zeroable input for non-master-key derivation
|
||||
40551a5 keyforkd: require hardened derivation on two highest indexes
|
||||
```
|
||||
|
||||
### Changes in keyforkd-models:
|
||||
|
||||
```
|
||||
40551a5 keyforkd: require hardened derivation on two highest indexes
|
||||
```
|
||||
|
||||
# Keyfork v0.1.0
|
||||
|
||||
### Tagged releases:
|
||||
|
||||
* `keyfork-bin 0.1.0`
|
||||
* `keyfork-bug 0.1.0`
|
||||
* `keyfork-crossterm 0.27.1`
|
||||
* `keyfork-derive-key 0.1.0`
|
||||
* `keyfork-derive-openpgp 0.1.0`
|
||||
* `keyfork-derive-path-data 0.1.0`
|
||||
* `keyfork-derive-util 0.1.0`
|
||||
* `keyfork-entropy 0.1.0`
|
||||
* `keyfork-frame 0.1.0`
|
||||
* `keyfork-mnemonic-util 0.2.0`
|
||||
* `keyfork-prompt 0.1.0`
|
||||
* `keyfork-qrcode 0.1.0`
|
||||
* `keyfork-shard 0.1.0`
|
||||
* `keyfork-slip10-test-data 0.1.0`
|
||||
* `keyfork 0.1.0`
|
||||
* `keyfork-zbar-sys 0.1.0`
|
||||
* `keyfork-zbar 0.1.0`
|
||||
* `keyforkd-client 0.1.0`
|
||||
* `keyforkd-models 0.1.0`
|
||||
* `keyforkd 0.1.0`
|
||||
* `smex 0.1.0`
|
File diff suppressed because it is too large
Load Diff
55
Cargo.toml
55
Cargo.toml
|
@ -14,68 +14,15 @@ members = [
|
|||
"crates/qrcode/keyfork-qrcode",
|
||||
"crates/qrcode/keyfork-zbar",
|
||||
"crates/qrcode/keyfork-zbar-sys",
|
||||
"crates/util/keyfork-bin",
|
||||
"crates/util/keyfork-bug",
|
||||
"crates/util/keyfork-crossterm",
|
||||
"crates/util/keyfork-entropy",
|
||||
"crates/util/keyfork-frame",
|
||||
"crates/util/keyfork-mnemonic",
|
||||
"crates/util/keyfork-mnemonic-util",
|
||||
"crates/util/keyfork-prompt",
|
||||
"crates/util/keyfork-slip10-test-data",
|
||||
"crates/util/smex",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
# Keyfork dependencies
|
||||
keyforkd = { version = "0.1.1", path = "crates/daemon/keyforkd", registry = "distrust", default-features = false }
|
||||
keyforkd-client = { version = "0.2.0", path = "crates/daemon/keyforkd-client", registry = "distrust", default-features = false }
|
||||
keyforkd-models = { version = "0.2.0", path = "crates/daemon/keyforkd-models", registry = "distrust", default-features = false }
|
||||
keyfork-derive-key = { version = "0.1.1", path = "crates/derive/keyfork-derive-key", registry = "distrust", default-features = false }
|
||||
keyfork-derive-openpgp = { version = "0.1.2", path = "crates/derive/keyfork-derive-openpgp", registry = "distrust", default-features = false }
|
||||
keyfork-derive-path-data = { version = "0.1.1", path = "crates/derive/keyfork-derive-path-data", registry = "distrust", default-features = false }
|
||||
keyfork-derive-util = { version = "0.2.0", path = "crates/derive/keyfork-derive-util", registry = "distrust", default-features = false }
|
||||
keyfork-shard = { version = "0.2.2", path = "crates/keyfork-shard", registry = "distrust", default-features = false }
|
||||
keyfork-qrcode = { version = "0.1.1", path = "crates/qrcode/keyfork-qrcode", registry = "distrust", default-features = false }
|
||||
keyfork-zbar = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar", registry = "distrust", default-features = false }
|
||||
keyfork-zbar-sys = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar-sys", registry = "distrust", default-features = false }
|
||||
keyfork-bin = { version = "0.1.0", path = "crates/util/keyfork-bin", registry = "distrust", default-features = false }
|
||||
keyfork-bug = { version = "0.1.0", path = "crates/util/keyfork-bug", registry = "distrust", default-features = false }
|
||||
keyfork-crossterm = { version = "0.27.1", path = "crates/util/keyfork-crossterm", registry = "distrust", default-features = false }
|
||||
keyfork-entropy = { version = "0.1.1", path = "crates/util/keyfork-entropy", registry = "distrust", default-features = false }
|
||||
keyfork-frame = { version = "0.1.0", path = "crates/util/keyfork-frame", registry = "distrust", default-features = false }
|
||||
keyfork-mnemonic = { version = "0.4.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false }
|
||||
keyfork-prompt = { version = "0.1.1", path = "crates/util/keyfork-prompt", registry = "distrust", default-features = false }
|
||||
keyfork-slip10-test-data = { version = "0.1.0", path = "crates/util/keyfork-slip10-test-data", registry = "distrust", default-features = false }
|
||||
smex = { version = "0.1.0", path = "crates/util/smex", registry = "distrust", default-features = false }
|
||||
|
||||
# External dependencies
|
||||
|
||||
# Cryptography
|
||||
ed25519-dalek = "2.1.1"
|
||||
hmac = "0.12.1"
|
||||
k256 = { version = "0.13.3", default-features = false, features = ["std"] }
|
||||
sha2 = "0.10.8"
|
||||
|
||||
# OpenPGP
|
||||
card-backend-pcsc = "0.5.0"
|
||||
openpgp-card = { version = "0.4.1" }
|
||||
openpgp-card-sequoia = { version = "0.2.0", default-features = false }
|
||||
sequoia-openpgp = { version = "1.21.2", default-features = false, features = ["compression"] }
|
||||
|
||||
# Serialization
|
||||
bincode = "1.3.3"
|
||||
serde = { version= "1.0.195", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
|
||||
# Misc.
|
||||
anyhow = "1.0.79"
|
||||
hex-literal = "0.4.1"
|
||||
image = { version = "0.25.2", default-features = false }
|
||||
thiserror = "1.0.56"
|
||||
tokio = "1.35.1"
|
||||
v4l = "0.14.0"
|
||||
|
||||
[profile.dev.package.keyfork-qrcode]
|
||||
opt-level = 3
|
||||
debug = true
|
||||
|
|
18
Makefile
18
Makefile
|
@ -1,6 +1,3 @@
|
|||
.PHONY: default
|
||||
default: docs/book
|
||||
|
||||
BASE_REF ?= main
|
||||
HEAD_REF ?= HEAD
|
||||
|
||||
|
@ -13,21 +10,6 @@ define clone-repo
|
|||
test `git -C $(1) rev-parse HEAD` = $(3)
|
||||
endef
|
||||
|
||||
docs/book: docs/src/links.md $(shell find docs/src -type f -name '*.md')
|
||||
mdbook build docs
|
||||
mkdir -p docs/book/rustdoc
|
||||
cargo test --doc
|
||||
cargo doc --no-deps
|
||||
cp -r ${CARGO_TARGET_DIR}/doc/* docs/book/rustdoc/
|
||||
|
||||
docs/src/links.md: docs/src/links.md.template
|
||||
echo "<!-- DO NOT EDIT THIS FILE MANUALLY, edit links.md.template -->" > $@
|
||||
envsubst < $< >> $@
|
||||
|
||||
.PHONY: touch
|
||||
touch:
|
||||
touch docs/src/links.md.template
|
||||
|
||||
.PHONY: review
|
||||
review:
|
||||
$(eval BASE_REF_PARSED := $(shell git rev-parse $(BASE_REF)))
|
||||
|
|
|
@ -75,7 +75,7 @@ Note: The following features are proposed, and may not yet be implemented.
|
|||
* Offline
|
||||
* Will exit if network access is detected to force you to keep keys offline
|
||||
* Helps limit the risk of supply chain attacks
|
||||
* Intended for use with QubesOS Vault VM, [AirgapOS](https://git.distrust.co/public/airgap), etc
|
||||
* Intended for use with QubesOS Vault VM, AirgapOS, etc
|
||||
* Private keys are installed to HSMs/TEEs for use by online machines
|
||||
|
||||
## Install
|
||||
|
@ -178,8 +178,7 @@ keyfork recover mnemonic
|
|||
|
||||
This guide assumes you are sharding to an `N`-of-`M` system with `I` smart
|
||||
cards per shardholder. The variables will be used in the following commands as
|
||||
`$N`, `$M`, and `$I`. The smart card OpenPGP slots will be factory reset during
|
||||
the process.
|
||||
`$N`, `$M`, and `$I`. The smart cards will be factory reset during the process.
|
||||
|
||||
On an airgapped system, run the following command to generate a file containing
|
||||
encrypted shards of a generated seed:
|
||||
|
|
92
bacon.toml
92
bacon.toml
|
@ -1,92 +0,0 @@
|
|||
# This is a configuration file for the bacon tool
|
||||
#
|
||||
# Bacon repository: https://github.com/Canop/bacon
|
||||
# Complete help on configuration: https://dystroy.org/bacon/config/
|
||||
# You can also check bacon's own bacon.toml file
|
||||
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
|
||||
|
||||
default_job = "check"
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "check", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "check", "--all-targets", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
"--all-targets",
|
||||
"--color", "always",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy-unwrap]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
"--lib",
|
||||
"--color", "always",
|
||||
"--",
|
||||
"-W",
|
||||
"clippy::unwrap_used",
|
||||
"-W",
|
||||
"clippy::expect_used",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
# This job lets you run
|
||||
# - all tests: bacon test
|
||||
# - a specific test: bacon test -- config::test_default_files
|
||||
# - the tests of a package: bacon test -- -- -p config
|
||||
[jobs.test]
|
||||
command = [
|
||||
"cargo", "test", "--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = ["cargo", "doc", "--color", "always", "--no-deps"]
|
||||
need_stdout = false
|
||||
|
||||
# If the doc compiles, then it opens in your browser and bacon switches
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
|
||||
need_stdout = false
|
||||
on_success = "back" # so that we don't open the browser at each change
|
||||
|
||||
# You can run your application and have the result displayed in bacon,
|
||||
# *if* it makes sense for this crate.
|
||||
# Don't forget the `--color always` part or the errors won't be
|
||||
# properly parsed.
|
||||
# If your program never stops (eg a server), you may set `background`
|
||||
# to false to have the cargo run output immediately displayed instead
|
||||
# of waiting for program's end.
|
||||
[jobs.run]
|
||||
command = [
|
||||
"cargo", "run",
|
||||
"--color", "always",
|
||||
# put launch parameters for your program behind a `--` separator
|
||||
]
|
||||
need_stdout = true
|
||||
allow_warnings = true
|
||||
background = true
|
||||
|
||||
# This parameterized job runs the example of your choice, as soon
|
||||
# as the code compiles.
|
||||
# Call it as
|
||||
# bacon ex -- my-example
|
||||
[jobs.ex]
|
||||
command = ["cargo", "run", "--color", "always", "--example"]
|
||||
need_stdout = true
|
||||
allow_warnings = true
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
# a project, for example a shortcut to launch a specific job.
|
||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
||||
# should go in your personal global prefs.toml file instead.
|
||||
[keybindings]
|
||||
# alt-m = "job:my-job"
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "keyforkd-client"
|
||||
version = "0.2.1"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
@ -8,18 +8,18 @@ license = "MIT"
|
|||
|
||||
[features]
|
||||
default = ["ed25519", "secp256k1"]
|
||||
ed25519 = ["keyfork-derive-util/ed25519", "ed25519-dalek"]
|
||||
secp256k1 = ["keyfork-derive-util/secp256k1", "k256"]
|
||||
ed25519 = ["keyfork-derive-util/ed25519"]
|
||||
secp256k1 = ["keyfork-derive-util/secp256k1"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-derive-util = { workspace = true, default-features = false }
|
||||
keyfork-frame = { workspace = true }
|
||||
keyforkd-models = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
k256 = { workspace = true, default-features = false, features = ["std"], optional = true }
|
||||
ed25519-dalek = { workspace = true, optional = true }
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false }
|
||||
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame" }
|
||||
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" }
|
||||
bincode = "1.3.3"
|
||||
thiserror = "1.0.49"
|
||||
|
||||
[dev-dependencies]
|
||||
keyfork-slip10-test-data = { workspace = true }
|
||||
keyforkd = { workspace = true }
|
||||
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data" }
|
||||
keyforkd = { path = "../keyforkd" }
|
||||
tempfile = "3.9.0"
|
||||
tokio = { version = "1.32.0", features = ["rt", "sync", "rt-multi-thread"] }
|
||||
|
|
|
@ -1,185 +1,9 @@
|
|||
//! # The Keyforkd Client
|
||||
//!
|
||||
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation
|
||||
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". This allows
|
||||
//! enforcement of policies, such as requiring at least two leves of a derivation path (for
|
||||
//! instance, `m/0'` would not be allowed, but `m/0'/0'` would). The server is operated on a UNIX
|
||||
//! socket with messages sent using the Keyfork Frame format.
|
||||
//!
|
||||
//! Programs using the Keyfork Client should ensure they are built against a compatible version of
|
||||
//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
|
||||
//! `0.5.x`) will be compatible, but `0.5.x` will not be compatible with `0.6.x`. For versions
|
||||
//! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but
|
||||
//! `1.x.y` will not be compatible with `2.0.0`.
|
||||
//!
|
||||
//! The Keyfork Client documentation makes extensive use of the `keyforkd::test_util` module.
|
||||
//! This provides testing infrastructure to set up a temporary Keyfork Daemon. In
|
||||
//! your code, you should assume the daemon has already been initialized, whether by another
|
||||
//! process, on another terminal, or some other instance. At no point should a program deriving an
|
||||
//! "endpoint" key have control over a mnemonic or a seed.
|
||||
//!
|
||||
//! ## Server Requests
|
||||
//!
|
||||
//! Keyfork is designed as a client-request/server-response model. The client sends a request, such
|
||||
//! as a derivation request, and the server sends its response. Presently, the Keyfork server
|
||||
//! supports the following requests:
|
||||
//!
|
||||
//! ### Request: Derive Key
|
||||
//!
|
||||
//! The client creates a derivation path of at least two indices and requests a derived XPrv
|
||||
//! (Extended Private Key) from the server.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! use keyforkd_client::Client;
|
||||
//! use keyfork_derive_util::DerivationPath;
|
||||
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
||||
//! // use k256::SecretKey as PrivateKey;
|
||||
//! // use ed25519_dalek::SigningKey as PrivateKey;
|
||||
//!
|
||||
//! #[derive(Debug, thiserror::Error)]
|
||||
//! enum Error {
|
||||
//! #[error(transparent)]
|
||||
//! Path(#[from] keyfork_derive_util::PathError),
|
||||
//!
|
||||
//! #[error(transparent)]
|
||||
//! Keyforkd(#[from] keyforkd_client::Error),
|
||||
//! }
|
||||
//!
|
||||
//! fn main() -> Result<(), Error> {
|
||||
//! # let seed = b"funky accordion noises";
|
||||
//! # keyforkd::test_util::run_test(seed, |socket_path| {
|
||||
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
|
||||
//! let mut client = Client::discover_socket()?;
|
||||
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
|
||||
//! # Ok::<_, Error>(())
|
||||
//! # })?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! Request objects are typically handled by the Keyfork Client library (such as with
|
||||
//! [`Client::request_xprv`]). While unadvised, clients can also attempt to handle their own
|
||||
//! requests, using [`Client::request`].
|
||||
//!
|
||||
//! ## Extended Private Keys
|
||||
//!
|
||||
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
|
||||
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed.
|
||||
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
|
||||
//! will be derived similarly between the server and the client.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! use keyforkd_client::Client;
|
||||
//! use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
||||
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
||||
//! // use k256::SecretKey as PrivateKey;
|
||||
//! // use ed25519_dalek::SigningKey as PrivateKey;
|
||||
//! # fn check_wallet<T>(_: T) {}
|
||||
//!
|
||||
//! #[derive(Debug, thiserror::Error)]
|
||||
//! enum Error {
|
||||
//! #[error(transparent)]
|
||||
//! Index(#[from] keyfork_derive_util::IndexError),
|
||||
//!
|
||||
//! #[error(transparent)]
|
||||
//! Path(#[from] keyfork_derive_util::PathError),
|
||||
//!
|
||||
//! #[error(transparent)]
|
||||
//! PrivateKey(#[from] keyfork_derive_util::PrivateKeyError),
|
||||
//!
|
||||
//! #[error(transparent)]
|
||||
//! Keyforkd(#[from] keyforkd_client::Error),
|
||||
//! }
|
||||
//!
|
||||
//! fn main() -> Result<(), Error> {
|
||||
//! # let seed = b"funky accordion noises";
|
||||
//! # keyforkd::test_util::run_test(seed, |socket_path| {
|
||||
//! let derivation_path = DerivationPath::from_str("m/44'/0'/0'/0")?;
|
||||
//! let mut client = Client::discover_socket()?;
|
||||
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
|
||||
//! // scan first 20 wallets
|
||||
//! for index in 0..20 {
|
||||
//! // use non-hardened derivation
|
||||
//! let new_xprv = xprv.derive_child(&DerivationIndex::new(index, false)?);
|
||||
//! check_wallet(new_xprv)
|
||||
//! }
|
||||
//! # Ok::<_, Error>(())
|
||||
//! # })?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Testing Infrastructure
|
||||
//!
|
||||
//! In tests, the `keyforkd::test_util` module and TestPrivateKeys can be used. These provide
|
||||
//! useful utilities for writing tests that interact with the Keyfork Server without needing to
|
||||
//! manually create the server for the purpose of the test. The `run_test` method can be used to
|
||||
//! run a test, which can handle both returning errors and correctly translating panics (though,
|
||||
//! the panics definitely won't look tidy).
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! use keyforkd_client::Client;
|
||||
//! use keyfork_derive_util::DerivationPath;
|
||||
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
||||
//!
|
||||
//! #[derive(Debug, thiserror::Error)]
|
||||
//! enum Error {
|
||||
//! #[error(transparent)]
|
||||
//! Path(#[from] keyfork_derive_util::PathError),
|
||||
//!
|
||||
//! #[error(transparent)]
|
||||
//! Keyforkd(#[from] keyforkd_client::Error),
|
||||
//! }
|
||||
//!
|
||||
//! fn main() -> Result<(), Error> {
|
||||
//! let seed = b"funky accordion noises";
|
||||
//! keyforkd::test_util::run_test(seed, |socket_path| {
|
||||
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
|
||||
//! let mut client = Client::discover_socket()?;
|
||||
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
|
||||
//! Ok::<_, Error>(())
|
||||
//! })?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you would rather write tests to panic rather than error, or would rather not deal with error
|
||||
//! types, the Panicable type should be used, which will handle the Error type for the closure.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! use keyforkd_client::Client;
|
||||
//! use keyfork_derive_util::DerivationPath;
|
||||
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
||||
//!
|
||||
//! let seed = b"funky accordion noises";
|
||||
//! keyforkd::test_util::run_test(seed, |socket_path| {
|
||||
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
||||
//! let mut client = Client::discover_socket().unwrap();
|
||||
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
||||
//! keyforkd::test_util::Panicable::Ok(())
|
||||
//! }).unwrap();
|
||||
//! ```
|
||||
//! A client for Keyforkd.
|
||||
|
||||
pub use std::os::unix::net::UnixStream;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use keyfork_derive_util::{
|
||||
request::{AsAlgorithm, DerivationRequest},
|
||||
DerivationPath, ExtendedPrivateKey, PrivateKey,
|
||||
};
|
||||
use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf};
|
||||
|
||||
use keyfork_frame::{try_decode_from, try_encode_to, DecodeError, EncodeError};
|
||||
use keyforkd_models::{Error as KeyforkdError, Request, Response};
|
||||
use keyforkd_models::{Request, Response, Error as KeyforkdError};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
@ -187,10 +11,6 @@ mod tests;
|
|||
/// An error occurred while interacting with Keyforkd.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The response from the server did not match the request.
|
||||
#[error("The response from the server did not match the request")]
|
||||
InvalidResponse,
|
||||
|
||||
/// The environment variables used for determining a Keyforkd socket path were not set.
|
||||
#[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")]
|
||||
EnvVarsNotFound,
|
||||
|
@ -217,11 +37,7 @@ pub enum Error {
|
|||
|
||||
/// An error encountered in Keyforkd.
|
||||
#[error("Error in Keyforkd: {0}")]
|
||||
Keyforkd(#[from] KeyforkdError),
|
||||
|
||||
/// An invalid key was returned.
|
||||
#[error("Invalid key returned")]
|
||||
InvalidKey,
|
||||
Keyforkd(#[from] KeyforkdError)
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
|
@ -265,21 +81,7 @@ pub struct Client {
|
|||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client from a given already-connected [`UnixStream`]. This function is
|
||||
/// provided in case a specific UnixStream has to be used; otherwise,
|
||||
/// [`Client::discover_socket`] should be preferred.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use keyforkd_client::{Client, get_socket};
|
||||
///
|
||||
/// # let seed = b"funky accordion noises";
|
||||
/// # keyforkd::test_util::run_test(seed, |socket_path| {
|
||||
/// let mut socket = get_socket()?;
|
||||
/// let mut client = Client::new(socket);
|
||||
/// # Ok::<_, keyforkd_client::Error>(())
|
||||
/// # }).unwrap();
|
||||
/// ```
|
||||
/// Create a new client from a given already-connected [`UnixStream`].
|
||||
pub fn new(socket: UnixStream) -> Self {
|
||||
Self { socket }
|
||||
}
|
||||
|
@ -289,77 +91,12 @@ impl Client {
|
|||
/// # Errors
|
||||
/// An error may be returned if the required environment variables were not set or if the
|
||||
/// socket could not be connected to.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use keyforkd_client::Client;
|
||||
///
|
||||
/// # let seed = b"funky accordion noises";
|
||||
/// # keyforkd::test_util::run_test(seed, |socket_path| {
|
||||
/// let mut client = Client::discover_socket()?;
|
||||
/// # Ok::<_, keyforkd_client::Error>(())
|
||||
/// # }).unwrap();
|
||||
/// ```
|
||||
pub fn discover_socket() -> Result<Self> {
|
||||
get_socket().map(|socket| Self { socket })
|
||||
}
|
||||
|
||||
/// Request an [`ExtendedPrivateKey`] for a given [`DerivationPath`].
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if:
|
||||
/// * Reading or writing from or to the socket encountered an error.
|
||||
/// * Bincode could not serialize the request or deserialize the response.
|
||||
/// * An error occurred in Keyforkd.
|
||||
/// * Keyforkd returned invalid data.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use keyforkd_client::Client;
|
||||
/// use keyfork_derive_util::DerivationPath;
|
||||
/// # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
||||
/// // use k256::SecretKey as PrivateKey;
|
||||
/// // use ed25519_dalek::SigningKey as PrivateKey;
|
||||
///
|
||||
/// # let seed = b"funky accordion noises";
|
||||
/// # keyforkd::test_util::run_test(seed, |socket_path| {
|
||||
/// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
||||
/// let mut client = Client::discover_socket().unwrap();
|
||||
/// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
||||
/// # keyforkd::test_util::Panicable::Ok(())
|
||||
/// # }).unwrap();
|
||||
/// ```
|
||||
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
|
||||
where
|
||||
K: PrivateKey + Clone + AsAlgorithm,
|
||||
{
|
||||
let algo = K::as_algorithm();
|
||||
let request = Request::Derivation(DerivationRequest::new(algo.clone(), path));
|
||||
let response = self.request(&request)?;
|
||||
match response {
|
||||
Response::Derivation(d) => {
|
||||
if d.algorithm != algo {
|
||||
return Err(Error::InvalidResponse);
|
||||
}
|
||||
|
||||
let depth = path.len() as u8;
|
||||
ExtendedPrivateKey::from_parts(&d.data, depth, d.chain_code)
|
||||
.map_err(|_| Error::InvalidKey)
|
||||
}
|
||||
_ => Err(Error::InvalidResponse),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
|
||||
///
|
||||
/// This function does not properly assert the association between a request type and a
|
||||
/// response type, and does not perform any serialization of native objects into Request or
|
||||
/// Response types, and should only be used when absolutely necessary.
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if:
|
||||
/// * Reading or writing from or to the socket encountered an error.
|
||||
|
|
|
@ -1,116 +1,100 @@
|
|||
use crate::Client;
|
||||
use keyfork_derive_util::{request::*, DerivationPath};
|
||||
use keyfork_slip10_test_data::test_data;
|
||||
use keyforkd::test_util::{run_test, Panicable};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::{os::unix::net::UnixStream, str::FromStr};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "secp256k1")]
|
||||
fn secp256k1_test_suite() {
|
||||
use k256::SecretKey;
|
||||
|
||||
fn secp256k1() {
|
||||
let tests = test_data()
|
||||
.unwrap()
|
||||
.remove("secp256k1")
|
||||
.remove(&"secp256k1".to_string())
|
||||
.unwrap();
|
||||
|
||||
for seed_test in tests {
|
||||
let seed = seed_test.seed;
|
||||
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
|
||||
for test in seed_test.tests {
|
||||
let socket = UnixStream::connect(socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let chain = DerivationPath::from_str(test.chain).unwrap();
|
||||
let chain_len = chain.len();
|
||||
if chain_len < 2 {
|
||||
continue;
|
||||
}
|
||||
if chain.iter().take(2).any(|index| !index.is_hardened()) {
|
||||
continue;
|
||||
}
|
||||
// Consistency check: ensure the server and the client can each derive the same
|
||||
// key using an XPrv, for all but the last XPrv, which is verified after this
|
||||
for i in 2..chain_len {
|
||||
// FIXME: Keyfork will only allow one request per session
|
||||
let socket = UnixStream::connect(socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let path = DerivationPath::from_str(test.chain).unwrap();
|
||||
let left_path = path.inner()[..i]
|
||||
.iter()
|
||||
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
||||
let right_path = path.inner()[i..]
|
||||
.iter()
|
||||
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
||||
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
|
||||
let derived_xprv = xprv.derive_path(&right_path).unwrap();
|
||||
let socket = UnixStream::connect(socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
|
||||
assert_eq!(
|
||||
derived_xprv, keyforkd_xprv,
|
||||
"{left_path} + {right_path} != {path}"
|
||||
);
|
||||
}
|
||||
let req = DerivationRequest::new(
|
||||
DerivationAlgorithm::Secp256k1,
|
||||
&DerivationPath::from_str(test.chain).unwrap(),
|
||||
);
|
||||
let response =
|
||||
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
|
||||
assert_eq!(&response.data, test.private_key.as_slice());
|
||||
// note: since client is non async, can't be single threaded
|
||||
let rt = Builder::new_multi_thread().enable_io().build().unwrap();
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
for (i, per_seed) in tests.into_iter().enumerate() {
|
||||
let mut socket_name = i.to_string();
|
||||
socket_name.push_str("-keyforkd.sock");
|
||||
let socket_path = tempdir.path().join(socket_name);
|
||||
let (tx, rx) = channel();
|
||||
let handle = rt.spawn({
|
||||
let socket_path = socket_path.clone();
|
||||
async move {
|
||||
let seed = per_seed.seed.clone();
|
||||
let mut server = keyforkd::UnixServer::bind(&socket_path).unwrap();
|
||||
tx.send(()).unwrap();
|
||||
let service = keyforkd::ServiceBuilder::new()
|
||||
.layer(keyforkd::middleware::BincodeLayer::new())
|
||||
.service(keyforkd::Keyforkd::new(seed));
|
||||
server.run(service).await.unwrap();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
rx.recv().unwrap();
|
||||
|
||||
for test in &per_seed.tests {
|
||||
let socket = UnixStream::connect(&socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let chain = DerivationPath::from_str(test.chain).unwrap();
|
||||
if chain.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let req = DerivationRequest::new(
|
||||
DerivationAlgorithm::Secp256k1,
|
||||
&DerivationPath::from_str(test.chain).unwrap(),
|
||||
);
|
||||
let response =
|
||||
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
|
||||
assert_eq!(response.data, test.private_key);
|
||||
}
|
||||
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ed25519")]
|
||||
fn ed25519_test_suite() {
|
||||
use ed25519_dalek::SigningKey;
|
||||
fn ed25519() {
|
||||
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
|
||||
|
||||
let tests = test_data().unwrap().remove("ed25519").unwrap();
|
||||
|
||||
for seed_test in tests {
|
||||
let seed = seed_test.seed;
|
||||
run_test(&seed, move |socket_path| {
|
||||
for test in seed_test.tests {
|
||||
let socket = UnixStream::connect(socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let chain = DerivationPath::from_str(test.chain).unwrap();
|
||||
let chain_len = chain.len();
|
||||
if chain_len < 2 {
|
||||
continue;
|
||||
}
|
||||
for i in 2..chain_len {
|
||||
// Consistency check: ensure the server and the client can each derive the same
|
||||
// key using an XPrv, for all but the last XPrv, which is verified after this
|
||||
let path = DerivationPath::from_str(test.chain).unwrap();
|
||||
let left_path = path.inner()[..i]
|
||||
.iter()
|
||||
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
||||
let right_path = path.inner()[i..]
|
||||
.iter()
|
||||
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
|
||||
let xprv = dbg!(client.request_xprv::<SigningKey>(&left_path)).unwrap();
|
||||
let derived_xprv = xprv.derive_path(&right_path).unwrap();
|
||||
let keyforkd_xprv = client.request_xprv::<SigningKey>(&path).unwrap();
|
||||
assert_eq!(
|
||||
derived_xprv, keyforkd_xprv,
|
||||
"{left_path} + {right_path} != {path}"
|
||||
);
|
||||
}
|
||||
let req = DerivationRequest::new(
|
||||
DerivationAlgorithm::Ed25519,
|
||||
&DerivationPath::from_str(test.chain).unwrap(),
|
||||
);
|
||||
let response =
|
||||
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
|
||||
assert_eq!(&response.data, test.private_key.as_slice());
|
||||
let rt = Builder::new_multi_thread().enable_io().build().unwrap();
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
for (i, per_seed) in tests.into_iter().enumerate() {
|
||||
let mut socket_name = i.to_string();
|
||||
socket_name.push_str("-keyforkd.sock");
|
||||
let socket_path = tempdir.path().join(socket_name);
|
||||
let (tx, rx) = channel();
|
||||
let handle = rt.spawn({
|
||||
let socket_path = socket_path.clone();
|
||||
async move {
|
||||
let seed = per_seed.seed.clone();
|
||||
let mut server = keyforkd::UnixServer::bind(&socket_path).unwrap();
|
||||
tx.send(()).unwrap();
|
||||
let service = keyforkd::ServiceBuilder::new()
|
||||
.layer(keyforkd::middleware::BincodeLayer::new())
|
||||
.service(keyforkd::Keyforkd::new(seed));
|
||||
server.run(service).await.unwrap();
|
||||
}
|
||||
Panicable::Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
rx.recv().unwrap();
|
||||
|
||||
for test in &per_seed.tests {
|
||||
let socket = UnixStream::connect(&socket_path).unwrap();
|
||||
let mut client = Client::new(socket);
|
||||
let chain = DerivationPath::from_str(test.chain).unwrap();
|
||||
if chain.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let req = DerivationRequest::new(
|
||||
DerivationAlgorithm::Ed25519,
|
||||
&DerivationPath::from_str(test.chain).unwrap(),
|
||||
);
|
||||
let response =
|
||||
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
|
||||
assert_eq!(response.data, test.private_key);
|
||||
}
|
||||
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
[package]
|
||||
name = "keyforkd-models"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
keyfork-derive-util = { workspace = true, default-features = false }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false }
|
||||
serde = { version = "1.0.190", features = ["derive"] }
|
||||
thiserror = "1.0.50"
|
||||
|
|
|
@ -43,10 +43,6 @@ pub enum DerivationError {
|
|||
#[error("Invalid derivation length: Expected at least 2, actual: {0}")]
|
||||
InvalidDerivationLength(usize),
|
||||
|
||||
/// The derivation request did not use hardened derivation on the 2 highest indexes.
|
||||
#[error("Invalid derivation paths: expected index #{0} (1) to be hardened")]
|
||||
InvalidDerivationPath(usize, u32),
|
||||
|
||||
/// An error occurred while deriving data.
|
||||
#[error("Derivation error: {0}")]
|
||||
Derivation(String),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "keyforkd"
|
||||
version = "0.1.2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
|
@ -12,28 +12,26 @@ tracing = ["tower/tracing", "tokio/tracing", "dep:tracing", "dep:tracing-subscri
|
|||
multithread = ["tokio/rt-multi-thread"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-bug = { workspace = true }
|
||||
keyfork-derive-util = { workspace = true }
|
||||
keyfork-frame = { workspace = true, features = ["async"] }
|
||||
keyfork-mnemonic = { workspace = true }
|
||||
keyfork-derive-path-data = { workspace = true }
|
||||
keyforkd-models = { workspace = true }
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util" }
|
||||
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame" }
|
||||
keyfork-mnemonic-util = { version = "0.1.0", path = "../../util/keyfork-mnemonic-util" }
|
||||
keyfork-derive-path-data = { version = "0.1.0", path = "../../derive/keyfork-derive-path-data" }
|
||||
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" }
|
||||
|
||||
# Not personally audited
|
||||
bincode = { workspace = true }
|
||||
bincode = "1.3.3"
|
||||
|
||||
# Ecosystem trust, not personally audited
|
||||
tokio = { workspace = true, features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] }
|
||||
tokio = { version = "1.32.0", features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] }
|
||||
tracing = { version = "0.1.37", optional = true }
|
||||
tracing-error = { version = "0.2.0", optional = true }
|
||||
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] }
|
||||
tower = { version = "0.4.13", features = ["tokio", "util"] }
|
||||
|
||||
# Personally audited
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tempfile = { version = "3.10.0", default-features = false }
|
||||
thiserror = "1.0.47"
|
||||
serde = { version = "1.0.186", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = { workspace = true }
|
||||
keyfork-slip10-test-data = { workspace = true }
|
||||
hex-literal = "0.4.1"
|
||||
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data" }
|
||||
|
|
|
@ -34,15 +34,3 @@ request, as well as its best-effort guess on what path is being derived (using
|
|||
the `keyfork-derive-path-data` crate), to inform the user of what keys are
|
||||
requested. Once the server sends the client the new extended private key, the
|
||||
client can then choose to use the key as-is, or derive further keys.
|
||||
|
||||
## Testing
|
||||
|
||||
A Keyfork server can be automatically started by using [`test_util::run_test`].
|
||||
The function accepts a closure, starting the server before the closure is run,
|
||||
and closing the server after the closure has completed. This may be useful for
|
||||
people writing software that interacts with the Keyfork server, such as a
|
||||
deriver or a provisioner. A test seed must be provided, but can be any content.
|
||||
The closure accepts one argument, the path of the UNIX socket from which the
|
||||
server can be accessed.
|
||||
|
||||
Examples of the test utility can be seen in the `keyforkd-client` crate.
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub use keyfork_mnemonic::Mnemonic;
|
||||
pub use keyfork_mnemonic_util::Mnemonic;
|
||||
pub use tower::ServiceBuilder;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
|
@ -30,8 +30,6 @@ pub use error::Keyforkd as KeyforkdError;
|
|||
pub use server::UnixServer;
|
||||
pub use service::Keyforkd;
|
||||
|
||||
pub mod test_util;
|
||||
|
||||
/// Set up a Tracing subscriber, defaulting to debug mode.
|
||||
#[cfg(feature = "tracing")]
|
||||
pub fn setup_registry() {
|
||||
|
@ -57,7 +55,7 @@ pub async fn start_and_run_server_on(
|
|||
let service = ServiceBuilder::new()
|
||||
.layer(middleware::BincodeLayer::new())
|
||||
// TODO: passphrase support and/or store passphrase with mnemonic
|
||||
.service(Keyforkd::new(mnemonic.generate_seed(None).to_vec()));
|
||||
.service(Keyforkd::new(mnemonic.seed(None)?));
|
||||
|
||||
let mut server = match UnixServer::bind(socket_path) {
|
||||
Ok(s) => s,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! Launch the Keyfork Server from using a mnemonic passed through standard input.
|
||||
//!
|
||||
|
||||
use keyfork_mnemonic::Mnemonic;
|
||||
use keyfork_mnemonic_util::Mnemonic;
|
||||
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
//! A UNIX socket server to run a Tower Service.
|
||||
|
||||
use keyfork_frame::{
|
||||
asyncext::{try_decode_from, try_encode_to},
|
||||
DecodeError, EncodeError,
|
||||
};
|
||||
use keyfork_frame::asyncext::{try_decode_from, try_encode_to};
|
||||
use std::{
|
||||
io::Error,
|
||||
path::{Path, PathBuf},
|
||||
|
@ -20,34 +17,6 @@ pub struct UnixServer {
|
|||
listener: UnixListener,
|
||||
}
|
||||
|
||||
/// This feels like a hack, but this is a convenient way to use the same method to quickly verify
|
||||
/// something across two different error types.
|
||||
trait IsDisconnect {
|
||||
fn is_disconnect(&self) -> bool;
|
||||
}
|
||||
|
||||
impl IsDisconnect for DecodeError {
|
||||
fn is_disconnect(&self) -> bool {
|
||||
if let Self::Io(e) = self {
|
||||
if let std::io::ErrorKind::UnexpectedEof = e.kind() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl IsDisconnect for EncodeError {
|
||||
fn is_disconnect(&self) -> bool {
|
||||
if let Self::Io(e) = self {
|
||||
if let std::io::ErrorKind::UnexpectedEof = e.kind() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl UnixServer {
|
||||
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also creates a ctrl_c handler to automatically clean up the socket file.
|
||||
///
|
||||
|
@ -99,85 +68,55 @@ impl UnixServer {
|
|||
#[cfg(feature = "tracing")]
|
||||
debug!("new socket connected");
|
||||
tokio::spawn(async move {
|
||||
let mut has_processed_request = false;
|
||||
// Process requests until an error occurs or a client disconnects
|
||||
loop {
|
||||
let bytes = match try_decode_from(&mut socket).await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
if e.is_disconnect() {
|
||||
#[cfg(feature = "tracing")]
|
||||
if !has_processed_request {
|
||||
debug!("client disconnected before sending any response");
|
||||
}
|
||||
return;
|
||||
}
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Error reading DerivationPath from socket");
|
||||
let content = e.to_string().bytes().collect::<Vec<_>>();
|
||||
let result = try_encode_to(&content[..], &mut socket).await;
|
||||
#[cfg(feature = "tracing")]
|
||||
if let Err(error) = result {
|
||||
debug!(%error, "Error sending error to client");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let app = match app.ready().await {
|
||||
Ok(app) => app,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Could not poll ready");
|
||||
let content = e.to_string().bytes().collect::<Vec<_>>();
|
||||
let result = try_encode_to(&content[..], &mut socket).await;
|
||||
#[cfg(feature = "tracing")]
|
||||
if let Err(error) = result {
|
||||
debug!(%error, "Error sending error to client");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let response = match app.call(bytes.into()).await {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Error reading DerivationPath from socket");
|
||||
let content = e.to_string().bytes().collect::<Vec<_>>();
|
||||
let result = try_encode_to(&content[..], &mut socket).await;
|
||||
#[cfg(feature = "tracing")]
|
||||
if let Err(error) = result {
|
||||
if error.is_disconnect() {
|
||||
#[cfg(feature = "tracing")]
|
||||
if has_processed_request {
|
||||
debug!("client disconnected while sending error frame");
|
||||
}
|
||||
return;
|
||||
}
|
||||
debug!(%error, "Error sending error to client");
|
||||
}
|
||||
has_processed_request = true;
|
||||
// The error has been successfully sent, the client may perform
|
||||
// another request.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
.into();
|
||||
|
||||
if let Err(e) = try_encode_to(&response[..], &mut socket).await {
|
||||
if e.is_disconnect() {
|
||||
#[cfg(feature = "tracing")]
|
||||
if has_processed_request {
|
||||
debug!("client disconnected while sending success frame");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let bytes = match try_decode_from(&mut socket).await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Error sending response to client");
|
||||
debug!(%e, "Error reading DerivationPath from socket");
|
||||
let content = e.to_string().bytes().collect::<Vec<_>>();
|
||||
let result = try_encode_to(&content[..], &mut socket).await;
|
||||
#[cfg(feature = "tracing")]
|
||||
if let Err(error) = result {
|
||||
debug!(%error, "Error sending error to client");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
has_processed_request = true;
|
||||
let app = match app.ready().await {
|
||||
Ok(app) => app,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Could not poll ready");
|
||||
let content = e.to_string().bytes().collect::<Vec<_>>();
|
||||
let result = try_encode_to(&content[..], &mut socket).await;
|
||||
#[cfg(feature = "tracing")]
|
||||
if let Err(error) = result {
|
||||
debug!(%error, "Error sending error to client");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let response = match app.call(bytes.into()).await {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Error reading DerivationPath from socket");
|
||||
let content = e.to_string().bytes().collect::<Vec<_>>();
|
||||
let result = try_encode_to(&content[..], &mut socket).await;
|
||||
#[cfg(feature = "tracing")]
|
||||
if let Err(error) = result {
|
||||
debug!(%error, "Error sending error to client");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
.into();
|
||||
|
||||
if let Err(e) = try_encode_to(&response[..], &mut socket).await {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Error sending response to client");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use keyfork_derive_path_data::guess_target;
|
|||
// use keyfork_derive_util::request::{DerivationError, DerivationRequest, DerivationResponse};
|
||||
use keyforkd_models::{DerivationError, Error, Request, Response};
|
||||
use tower::Service;
|
||||
use tracing::{info, warn};
|
||||
use tracing::info;
|
||||
|
||||
// NOTE: All values implemented in Keyforkd must implement Clone with low overhead, either by
|
||||
// using an Arc or by having a small signature. This is because Service<T> takes &mut self.
|
||||
|
@ -38,9 +38,6 @@ impl std::fmt::Debug for Keyforkd {
|
|||
impl Keyforkd {
|
||||
/// Create a new instance of Keyfork from a given seed.
|
||||
pub fn new(seed: Vec<u8>) -> Self {
|
||||
if seed.len() < 16 {
|
||||
warn!("Entropy size is lower than 128 bits: {} bits.", seed.len() * 8);
|
||||
}
|
||||
Self {
|
||||
seed: Arc::new(seed),
|
||||
}
|
||||
|
@ -72,18 +69,6 @@ impl Service<Request> for Keyforkd {
|
|||
return Err(DerivationError::InvalidDerivationLength(len).into());
|
||||
}
|
||||
|
||||
if let Some((i, unhardened_index)) = req
|
||||
.path()
|
||||
.iter()
|
||||
.take(2)
|
||||
.enumerate()
|
||||
.find(|(_, index)| {
|
||||
!index.is_hardened()
|
||||
})
|
||||
{
|
||||
return Err(DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into())
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
if let Some(target) = guess_target(req.path()) {
|
||||
info!("Deriving path: {target}");
|
||||
|
@ -91,7 +76,7 @@ impl Service<Request> for Keyforkd {
|
|||
info!("Deriving path: {}", req.path());
|
||||
}
|
||||
|
||||
req.derive_with_master_seed(seed.as_ref())
|
||||
req.derive_with_master_seed((*seed).clone())
|
||||
.map(Response::Derivation)
|
||||
.map_err(|e| DerivationError::Derivation(e.to_string()).into())
|
||||
}),
|
||||
|
@ -113,7 +98,7 @@ mod tests {
|
|||
async fn properly_derives_secp256k1() {
|
||||
let tests = test_data()
|
||||
.unwrap()
|
||||
.remove("secp256k1")
|
||||
.remove(&"secp256k1".to_string())
|
||||
.unwrap();
|
||||
|
||||
for per_seed in tests {
|
||||
|
@ -125,9 +110,6 @@ mod tests {
|
|||
if chain.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
if chain.iter().take(2).any(|index| !index.is_hardened()) {
|
||||
continue;
|
||||
}
|
||||
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
|
||||
let response: DerivationResponse = keyforkd
|
||||
.ready()
|
||||
|
@ -138,7 +120,7 @@ mod tests {
|
|||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
assert_eq!(&response.data, test.private_key.as_slice());
|
||||
assert_eq!(response.data, test.private_key);
|
||||
assert_eq!(response.chain_code.as_slice(), test.chain_code);
|
||||
}
|
||||
}
|
||||
|
@ -146,7 +128,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn properly_derives_ed25519() {
|
||||
let tests = test_data().unwrap().remove("ed25519").unwrap();
|
||||
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
|
||||
|
||||
for per_seed in tests {
|
||||
let seed = &per_seed.seed;
|
||||
|
@ -168,7 +150,7 @@ mod tests {
|
|||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
assert_eq!(&response.data, test.private_key.as_slice());
|
||||
assert_eq!(response.data, test.private_key);
|
||||
assert_eq!(response.chain_code.as_slice(), test.chain_code);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
//! # Keyforkd Test Utilities
|
||||
//!
|
||||
//! This module adds a helper to set up a Tokio runtime, start a Tokio runtime with a given seed,
|
||||
//! start a Keyfork server on that runtime, and run a given test closure.
|
||||
|
||||
use crate::{middleware, Keyforkd, ServiceBuilder, UnixServer};
|
||||
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
use keyfork_bug::bug;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("This error can never be instantiated")]
|
||||
#[doc(hidden)]
|
||||
pub enum UninstantiableError {}
|
||||
|
||||
/// A panicable result. This type can be used when a closure chooses to panic instead of
|
||||
/// returning an error. This doesn't necessarily mean a closure _has_ to panic, and its absence
|
||||
/// doesn't imply a closure _can't_ panic, but this is a useful utility function for writing tests,
|
||||
/// to avoid the necessity of making custom error types.
|
||||
///
|
||||
/// ```rust
|
||||
/// use keyforkd::test_util::Panicable;
|
||||
/// let closure = || {
|
||||
/// Panicable::Ok(())
|
||||
/// };
|
||||
/// assert!(closure().is_ok());
|
||||
/// ```
|
||||
pub type Panicable<T> = std::result::Result<T, UninstantiableError>;
|
||||
|
||||
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
|
||||
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
|
||||
/// the server is capable of being interacted with. The test is in the form of a closure, expected
|
||||
/// to return a [`Result`] where success is a unit type (test passed) and the error is any error
|
||||
/// that happened during the test (alternatively, a panic may be used, and will be returned as an
|
||||
/// error).
|
||||
///
|
||||
/// # Panics
|
||||
/// The function may panic if any errors arise while configuring and using the Tokio multithreaded
|
||||
/// runtime.
|
||||
///
|
||||
/// # Examples
|
||||
/// The test utility provides a socket that can be connected to for deriving keys.
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::os::unix::net::UnixStream;
|
||||
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
|
||||
/// UnixStream::connect(&path).map(|_| ())
|
||||
/// }).unwrap();
|
||||
/// ```
|
||||
///
|
||||
/// The `keyforkd-client` crate uses the `KEYFORKD_SOCKET_PATH` variable to determine the default
|
||||
/// socket path. The test will export the environment variable so it may be used by default.
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::os::unix::net::UnixStream;
|
||||
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
|
||||
/// assert_eq!(std::env::var_os("KEYFORKD_SOCKET_PATH").unwrap(), path.as_os_str());
|
||||
/// UnixStream::connect(&path).map(|_| ())
|
||||
/// }).unwrap();
|
||||
/// ```
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
|
||||
where
|
||||
F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
|
||||
E: Send + 'static,
|
||||
{
|
||||
let rt = Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect(bug!(
|
||||
"can't make tokio threaded IO runtime, should be enabled via feature flags"
|
||||
));
|
||||
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
|
||||
let socket_path = socket_dir.path().join("keyforkd.sock");
|
||||
let result = rt.block_on(async move {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
||||
let server_handle = tokio::spawn({
|
||||
let socket_path = socket_path.clone();
|
||||
let seed = seed.to_vec();
|
||||
async move {
|
||||
let mut server =
|
||||
UnixServer::bind(&socket_path).expect(bug!("can't bind unix socket"));
|
||||
tx.send(())
|
||||
.await
|
||||
.expect(bug!("couldn't send server start signal"));
|
||||
let service = ServiceBuilder::new()
|
||||
.layer(middleware::BincodeLayer::new())
|
||||
.service(Keyforkd::new(seed.to_vec()));
|
||||
server.run(service).await.expect(bug!("Unable to start service"));
|
||||
}
|
||||
});
|
||||
|
||||
rx.recv()
|
||||
.await
|
||||
.expect(bug!("can't receive server start signal from channel"));
|
||||
std::env::set_var("KEYFORKD_SOCKET_PATH", &socket_path);
|
||||
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
|
||||
|
||||
let result = test_handle.await;
|
||||
server_handle.abort();
|
||||
result
|
||||
});
|
||||
if let Err(e) = result {
|
||||
if let Ok(reason) = e.try_into_panic() {
|
||||
std::panic::resume_unwind(reason);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_run_test() {
|
||||
let seed = b"beefbeef";
|
||||
run_test(seed, |_path| Panicable::Ok(())).expect("infallible");
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
[package]
|
||||
name = "keyfork-derive-age"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] }
|
||||
keyforkd-client = { workspace = true }
|
||||
smex = { workspace = true }
|
||||
thiserror = "1.0.48"
|
||||
bech32 = "0.11.0"
|
||||
keyfork-derive-path-data = { workspace = true }
|
||||
ed25519-dalek = "2.1.1"
|
|
@ -1,69 +0,0 @@
|
|||
use std::{env, process::ExitCode, str::FromStr};
|
||||
|
||||
use keyfork_derive_path_data::paths;
|
||||
use keyfork_derive_util::{DerivationPath, ExtendedPrivateKey, PathError};
|
||||
use keyforkd_client::Client;
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
||||
type XPrv = ExtendedPrivateKey<SigningKey>;
|
||||
|
||||
/// Any error that can occur while deriving a key.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The given path could not be parsed.
|
||||
#[error("Could not parse the given path: {0}")]
|
||||
PathFormat(#[from] PathError),
|
||||
|
||||
/// The request to derive data failed.
|
||||
#[error("Unable to perform key derivation request: {0}")]
|
||||
KeyforkdClient(#[from] keyforkd_client::Error),
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
fn validate(path: &str) -> Result<DerivationPath> {
|
||||
let index = paths::AGE.inner().first().unwrap();
|
||||
|
||||
let path = DerivationPath::from_str(path)?;
|
||||
assert!(
|
||||
path.len() >= 2,
|
||||
"Expected path of at least m/{index}/account_id'"
|
||||
);
|
||||
|
||||
let given_index = path.iter().next().expect("checked .len() above");
|
||||
assert_eq!(
|
||||
index, given_index,
|
||||
"Expected derivation path starting with m/{index}, got: {given_index}",
|
||||
);
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = env::args();
|
||||
let program_name = args.next().expect("program name");
|
||||
let args = args.collect::<Vec<_>>();
|
||||
let path = match args.as_slice() {
|
||||
[path] => validate(path)?,
|
||||
_ => panic!("Usage: {program_name} path"),
|
||||
};
|
||||
|
||||
let mut client = Client::discover_socket()?;
|
||||
// TODO: should this key be clamped to Curve25519 specs?
|
||||
let xprv: XPrv = client.request_xprv(&path)?;
|
||||
let hrp = bech32::Hrp::parse("AGE-SECRET-KEY-")?;
|
||||
let age_key = bech32::encode::<bech32::Bech32>(hrp, &xprv.private_key().to_bytes())?;
|
||||
println!("{}", age_key.to_uppercase());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
if let Err(e) = run() {
|
||||
eprintln!("Error: {e}");
|
||||
ExitCode::FAILURE
|
||||
} else {
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
[package]
|
||||
name = "keyfork-derive-key"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
keyfork-derive-util = { workspace = true }
|
||||
keyforkd-client = { workspace = true }
|
||||
smex = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util" }
|
||||
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client" }
|
||||
smex = { version = "0.1.0", path = "../../util/smex" }
|
||||
thiserror = "1.0.48"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
//! Query the Keyfork Server to generate a hex-encoded key for a given algorithm.
|
||||
//!
|
||||
|
||||
use std::{env, process::ExitCode, str::FromStr};
|
||||
|
||||
use keyfork_derive_util::{
|
||||
request::{DerivationAlgorithm, DerivationError, DerivationRequest, DerivationResponse},
|
||||
DerivationPath, PathError,
|
||||
DerivationPath,
|
||||
};
|
||||
use keyforkd_client::Client;
|
||||
|
||||
|
@ -17,7 +17,7 @@ pub enum Error {
|
|||
|
||||
/// The given path could not be parsed.
|
||||
#[error("Could not parse the given path: {0}")]
|
||||
PathFormat(#[from] PathError),
|
||||
PathFormat(#[from] keyfork_derive_util::path::Error),
|
||||
|
||||
/// The request to derive data failed.
|
||||
#[error("Unable to perform key derivation request: {0}")]
|
||||
|
@ -46,7 +46,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let mut client = Client::discover_socket()?;
|
||||
let request = DerivationRequest::new(algo, &path);
|
||||
let response = client.request(&request.into())?;
|
||||
println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
|
||||
println!("{}", smex::encode(&DerivationResponse::try_from(response)?.data));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
[package]
|
||||
name = "keyfork-derive-openpgp"
|
||||
version = "0.1.3"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
default = ["bin"]
|
||||
default = []
|
||||
bin = ["sequoia-openpgp/crypto-nettle"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] }
|
||||
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] }
|
||||
ed25519-dalek = { workspace = true }
|
||||
sequoia-openpgp = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
keyfork-derive-path-data = { workspace = true }
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"] }
|
||||
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", default-features = false, features = ["ed25519"] }
|
||||
ed25519-dalek = "2.0.0"
|
||||
sequoia-openpgp = { version = "1.17.0", default-features = false }
|
||||
anyhow = "1.0.75"
|
||||
thiserror = "1.0.49"
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data.
|
||||
//! Creation of OpenPGP certificates from BIP-0032 derived data.
|
||||
|
||||
use std::{
|
||||
str::FromStr,
|
||||
time::{Duration, SystemTime, SystemTimeError},
|
||||
use std::time::{Duration, SystemTime, SystemTimeError};
|
||||
|
||||
use derive_util::{
|
||||
request::{DerivationResponse, TryFromDerivationResponseError},
|
||||
DerivationIndex, ExtendedPrivateKey, PrivateKey,
|
||||
};
|
||||
|
||||
use derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey};
|
||||
use ed25519_dalek::SigningKey;
|
||||
pub use keyfork_derive_util as derive_util;
|
||||
pub use sequoia_openpgp as openpgp;
|
||||
use sequoia_openpgp::{
|
||||
packet::{
|
||||
key::{Key4, PrimaryRole, SubordinateRole},
|
||||
|
@ -18,15 +17,7 @@ use sequoia_openpgp::{
|
|||
types::{KeyFlags, SignatureType},
|
||||
Cert, Packet,
|
||||
};
|
||||
|
||||
// TODO: this key type is actually _not_ the extended private key, so it should be renamed
|
||||
// something like Prv or PrvKey.
|
||||
|
||||
/// The private key type used with OpenPGP.
|
||||
pub type XPrvKey = SigningKey;
|
||||
|
||||
/// The extended private key type used with OpenPGP.
|
||||
pub type XPrv = ExtendedPrivateKey<XPrvKey>;
|
||||
pub use sequoia_openpgp as openpgp;
|
||||
|
||||
/// An error occurred while creating an OpenPGP key.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
@ -40,9 +31,13 @@ pub enum Error {
|
|||
#[error("Key configured with both encryption and non-encryption key flags: {0:?}")]
|
||||
InvalidKeyFlags(KeyFlags),
|
||||
|
||||
/// The derivation response contained incorrect data.
|
||||
#[error("Incorrect derived data: {0}")]
|
||||
IncorrectDerivedData(#[from] TryFromDerivationResponseError),
|
||||
|
||||
/// A derivation index could not be created from the given index.
|
||||
#[error("Could not create derivation index: {0}")]
|
||||
Index(#[from] IndexError),
|
||||
Index(#[from] keyfork_derive_util::index::Error),
|
||||
|
||||
/// A derivation operation could not be performed against the private key.
|
||||
#[error("Could not perform operation against private key: {0}")]
|
||||
|
@ -65,64 +60,41 @@ pub enum Error {
|
|||
#[allow(missing_docs)]
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
/// Create an OpenPGP Cert with private key data, with derived keys from the given derivation
|
||||
/// response, keys, and User ID.
|
||||
///
|
||||
/// Certificates are created with a default expiration of one day, but may be configured to expire
|
||||
/// later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as "15d" (15
|
||||
/// days), "1m" (one month), or "2y" (two years).
|
||||
/// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
|
||||
/// ID.
|
||||
///
|
||||
/// # Errors
|
||||
/// The function may error for any condition mentioned in [`Error`].
|
||||
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||
let primary_key_flags = match keys.first() {
|
||||
pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||
let primary_key_flags = match keys.get(0) {
|
||||
Some(kf) if kf.for_certification() => kf,
|
||||
_ => return Err(Error::NotCert),
|
||||
};
|
||||
|
||||
let epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
|
||||
let now = SystemTime::now();
|
||||
let expiration_date = match std::env::var("KEYFORK_OPENPGP_EXPIRE").as_mut() {
|
||||
Ok(var) => {
|
||||
let ch = var.pop();
|
||||
match (ch, u64::from_str(var)) {
|
||||
(Some(ch @ ('d' | 'm' | 'y')), Ok(expire)) => {
|
||||
let multiplier = match ch {
|
||||
'd' => 1,
|
||||
'm' => 30,
|
||||
'y' => 365,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
now + Duration::from_secs(60 * 60 * 24 * expire * multiplier)
|
||||
}
|
||||
_ => now + Duration::from_secs(60 * 60 * 24),
|
||||
}
|
||||
}
|
||||
Err(_) => now + Duration::from_secs(60 * 60 * 24),
|
||||
};
|
||||
let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
|
||||
|
||||
// Create certificate with initial key and signature
|
||||
let xprv = ExtendedPrivateKey::<SigningKey>::try_from(data)?;
|
||||
let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?;
|
||||
let mut primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519(
|
||||
let primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519(
|
||||
&PrivateKey::to_bytes(derived_primary_key.private_key()),
|
||||
epoch,
|
||||
)?);
|
||||
primary_key.set_creation_time(epoch)?;
|
||||
let cert = Cert::from_packets(vec![Packet::SecretKey(primary_key.clone())].into_iter())?;
|
||||
|
||||
// Sign and attach primary key and primary userid
|
||||
let builder = SignatureBuilder::new(SignatureType::PositiveCertification)
|
||||
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
|
||||
// .set_signature_creation_time(now)?
|
||||
.set_key_validity_period(one_day.duration_since(epoch)?)?
|
||||
.set_signature_creation_time(epoch)?
|
||||
.set_key_flags(primary_key_flags.clone())?;
|
||||
let binding = userid.bind(&mut primary_key.clone().into_keypair()?, &cert, builder)?;
|
||||
let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?;
|
||||
let policy = sequoia_openpgp::policy::StandardPolicy::new();
|
||||
|
||||
// Set certificate expiration to configured expiration or (default) one day
|
||||
// Set certificate expiration to one day
|
||||
let mut keypair = primary_key.clone().into_keypair()?;
|
||||
let signatures =
|
||||
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;
|
||||
let signatures = cert.set_expiration_time(&policy, None, &mut keypair, Some(one_day))?;
|
||||
let cert = cert.insert_packets(signatures)?;
|
||||
|
||||
let mut cert = cert;
|
||||
|
@ -135,7 +107,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
|||
let is_non_enc = subkey_flags.for_certification()
|
||||
|| subkey_flags.for_signing()
|
||||
|| subkey_flags.for_authentication();
|
||||
let mut subkey = if is_enc && is_non_enc {
|
||||
let subkey = if is_enc && is_non_enc {
|
||||
return Err(Error::InvalidKeyFlags(subkey_flags.clone()));
|
||||
} else if is_enc {
|
||||
// Clamp key before exporting as OpenPGP. Reference:
|
||||
|
@ -145,27 +117,32 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
|||
bytes[0] &= 0b1111_1000;
|
||||
bytes[31] &= !0b1000_0000;
|
||||
bytes[31] |= 0b0100_0000;
|
||||
Key::from(Key4::<_, SubordinateRole>::import_secret_cv25519(
|
||||
&bytes, None, None, epoch,
|
||||
)?)
|
||||
Key::from(
|
||||
Key4::<_, SubordinateRole>::import_secret_cv25519(
|
||||
&bytes,
|
||||
None,
|
||||
None,
|
||||
epoch,
|
||||
)?
|
||||
)
|
||||
} else {
|
||||
Key::from(Key4::<_, SubordinateRole>::import_secret_ed25519(
|
||||
&PrivateKey::to_bytes(derived_key.private_key()),
|
||||
epoch,
|
||||
)?)
|
||||
Key::from(
|
||||
Key4::<_, SubordinateRole>::import_secret_ed25519(
|
||||
&PrivateKey::to_bytes(derived_key.private_key()),
|
||||
epoch,
|
||||
)?
|
||||
)
|
||||
};
|
||||
|
||||
subkey.set_creation_time(epoch)?;
|
||||
|
||||
// As per OpenPGP spec, signing keys must backsig the primary key
|
||||
let builder = if subkey_flags.for_signing() {
|
||||
SignatureBuilder::new(SignatureType::SubkeyBinding)
|
||||
.set_key_flags(subkey_flags.clone())?
|
||||
// .set_signature_creation_time(epoch)?
|
||||
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
|
||||
.set_signature_creation_time(epoch)?
|
||||
.set_key_validity_period(one_day.duration_since(epoch)?)?
|
||||
.set_embedded_signature(
|
||||
SignatureBuilder::new(SignatureType::PrimaryKeyBinding)
|
||||
//.set_signature_creation_time(epoch)?
|
||||
.set_signature_creation_time(epoch)?
|
||||
.sign_primary_key_binding(
|
||||
&mut subkey.clone().into_keypair()?,
|
||||
&primary_key,
|
||||
|
@ -175,8 +152,8 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
|||
} else {
|
||||
SignatureBuilder::new(SignatureType::SubkeyBinding)
|
||||
.set_key_flags(subkey_flags.clone())?
|
||||
// .set_signature_creation_time(epoch)?
|
||||
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
|
||||
.set_signature_creation_time(epoch)?
|
||||
.set_key_validity_period(one_day.duration_since(epoch)?)?
|
||||
};
|
||||
|
||||
// Sign subkey with primary key and attach to cert
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
//! Query the Keyfork Servre to derive an OpenPGP Secret Key.
|
||||
//!
|
||||
|
||||
use std::{env, process::ExitCode, str::FromStr};
|
||||
|
||||
use keyfork_derive_util::DerivationPath;
|
||||
use keyfork_derive_path_data::paths;
|
||||
use keyforkd_client::Client;
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
use sequoia_openpgp::{
|
||||
armor::{Kind, Writer},
|
||||
packet::UserID,
|
||||
serialize::Marshal,
|
||||
types::KeyFlags,
|
||||
use keyfork_derive_util::{
|
||||
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
|
||||
DerivationIndex, DerivationPath,
|
||||
};
|
||||
use keyforkd_client::Client;
|
||||
use sequoia_openpgp::{packet::UserID, types::KeyFlags, armor::{Kind, Writer}, serialize::Marshal};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum Error {
|
||||
|
@ -79,14 +74,16 @@ fn validate(
|
|||
subkey_format: &str,
|
||||
default_userid: &str,
|
||||
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
|
||||
let index = paths::OPENPGP.inner().first().unwrap();
|
||||
let mut pgp_u32 = [0u8; 4];
|
||||
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
|
||||
let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||
|
||||
let path = DerivationPath::from_str(path)?;
|
||||
assert!(path.len() >= 2, "Expected path of at least m/{index}/account_id'");
|
||||
assert_eq!(2, path.len(), "Expected path of m/{index}/account_id'");
|
||||
|
||||
let given_index = path.iter().next().expect("checked .len() above");
|
||||
assert_eq!(
|
||||
index, given_index,
|
||||
&index, given_index,
|
||||
"Expected derivation path starting with m/{index}, got: {given_index}",
|
||||
);
|
||||
|
||||
|
@ -111,17 +108,20 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||
_ => panic!("Usage: {program_name} path subkey_format default_userid"),
|
||||
};
|
||||
|
||||
let derived_xprv = Client::discover_socket()?.request_xprv::<SigningKey>(&path)?;
|
||||
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
|
||||
let derived_data: DerivationResponse = Client::discover_socket()?
|
||||
.request(&request.into())?
|
||||
.try_into()?;
|
||||
let subkeys = subkey_format
|
||||
.iter()
|
||||
.map(|kt| kt.inner().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let cert = keyfork_derive_openpgp::derive(derived_xprv, subkeys.as_slice(), &default_userid)?;
|
||||
let cert = keyfork_derive_openpgp::derive(derived_data, subkeys.as_slice(), &default_userid)?;
|
||||
|
||||
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
|
||||
|
||||
for packet in cert.into_packets2() {
|
||||
for packet in cert.into_packets() {
|
||||
packet.serialize(&mut w)?;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
[package]
|
||||
name = "keyfork-derive-path-data"
|
||||
version = "0.1.2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
keyfork-derive-util = { workspace = true, default-features = false }
|
||||
once_cell = "1.19.0"
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false }
|
||||
|
|
|
@ -2,129 +2,32 @@
|
|||
|
||||
#![allow(clippy::unreadable_literal)]
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use keyfork_derive_util::{DerivationIndex, DerivationPath};
|
||||
|
||||
/// All common paths for key derivation.
|
||||
pub mod paths {
|
||||
use super::*;
|
||||
|
||||
/// The default derivation path for OpenPGP.
|
||||
pub static OPENPGP: Lazy<DerivationPath> = Lazy::new(|| {
|
||||
DerivationPath::default().chain_push(DerivationIndex::new_unchecked(
|
||||
u32::from_be_bytes(*b"\x00pgp"),
|
||||
true,
|
||||
))
|
||||
});
|
||||
|
||||
/// The derivation path for OpenPGP certificates used for sharding.
|
||||
pub static OPENPGP_SHARD: Lazy<DerivationPath> = Lazy::new(|| {
|
||||
DerivationPath::default()
|
||||
.chain_push(DerivationIndex::new_unchecked(
|
||||
u32::from_be_bytes(*b"\x00pgp"),
|
||||
true,
|
||||
))
|
||||
.chain_push(DerivationIndex::new_unchecked(
|
||||
u32::from_be_bytes(*b"shrd"),
|
||||
true,
|
||||
))
|
||||
});
|
||||
|
||||
/// The derivation path for OpenPGP certificates used for disaster recovery.
|
||||
pub static OPENPGP_DISASTER_RECOVERY: Lazy<DerivationPath> = Lazy::new(|| {
|
||||
DerivationPath::default()
|
||||
.chain_push(DerivationIndex::new_unchecked(
|
||||
u32::from_be_bytes(*b"\x00pgp"),
|
||||
true,
|
||||
))
|
||||
.chain_push(DerivationIndex::new_unchecked(
|
||||
u32::from_be_bytes(*b"\x00\x00dr"),
|
||||
true,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
/// Determine if a prefix matches and whether the next index exists.
|
||||
fn prefix_matches(given: &DerivationPath, target: &DerivationPath) -> Option<DerivationIndex> {
|
||||
if given.len() <= target.len() {
|
||||
return None;
|
||||
}
|
||||
if target
|
||||
.iter()
|
||||
.zip(given.iter())
|
||||
.all(|(left, right)| left == right)
|
||||
{
|
||||
given.iter().nth(target.len()).cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// The default derivation path for OpenPGP.
|
||||
pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true);
|
||||
|
||||
/// A derivation target.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Target {
|
||||
/// An OpenPGP key, whose account is the given index.
|
||||
OpenPGP(DerivationIndex),
|
||||
|
||||
/// An OpenPGP key used for sharding.
|
||||
OpenPGPShard(DerivationIndex),
|
||||
|
||||
/// An OpenPGP key used for disaster recovery.
|
||||
OpenPGPDisasterRecovery(DerivationIndex),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Target {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Target::OpenPGP(account) => {
|
||||
Self::OpenPGP(account) => {
|
||||
write!(f, "OpenPGP key (account {account})")
|
||||
}
|
||||
Target::OpenPGPShard(shard_index) => {
|
||||
write!(f, "OpenPGP Shard key (shard index {shard_index})")
|
||||
}
|
||||
Target::OpenPGPDisasterRecovery(account) => {
|
||||
write!(f, "OpenPGP Disaster Recovery key (account {account})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! test_match {
|
||||
($var:ident, $shard:path, $target:path) => {
|
||||
if let Some(index) = prefix_matches($var, &$shard) {
|
||||
return Some($target(index));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Determine the closest [`Target`] for the given path. This method is intended to be used by
|
||||
/// `keyforkd` to provide an optional textual prompt to what a client is attempting to derive.
|
||||
pub fn guess_target(path: &DerivationPath) -> Option<Target> {
|
||||
test_match!(path, paths::OPENPGP_SHARD, Target::OpenPGPShard);
|
||||
test_match!(
|
||||
path,
|
||||
paths::OPENPGP_DISASTER_RECOVERY,
|
||||
Target::OpenPGPDisasterRecovery
|
||||
);
|
||||
test_match!(path, paths::OPENPGP, Target::OpenPGP);
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let index = DerivationIndex::new(5312, false).unwrap();
|
||||
let dr_key = paths::OPENPGP_DISASTER_RECOVERY
|
||||
.clone()
|
||||
.chain_push(index.clone());
|
||||
match guess_target(&dr_key) {
|
||||
Some(Target::OpenPGPDisasterRecovery(idx)) if idx == index => (),
|
||||
bad => panic!("invalid value: {bad:?}"),
|
||||
}
|
||||
}
|
||||
Some(match path.iter().collect::<Vec<_>>()[..] {
|
||||
[t, index] if t == &OPENPGP => Target::OpenPGP(index.clone()),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "keyfork-derive-util"
|
||||
version = "0.2.1"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
@ -12,25 +12,24 @@ secp256k1 = ["k256"]
|
|||
ed25519 = ["ed25519-dalek"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-mnemonic = { workspace = true }
|
||||
keyfork-bug = { workspace = true }
|
||||
keyfork-mnemonic-util = { version = "0.1.0", path = "../../util/keyfork-mnemonic-util" }
|
||||
|
||||
# Included in Rust
|
||||
digest = "0.10.7"
|
||||
sha2 = { workspace = true }
|
||||
sha2 = "0.10.7"
|
||||
|
||||
# Rust-Crypto ecosystem, not personally audited
|
||||
ripemd = "0.1.3"
|
||||
hmac = { workspace = true, features = ["std"] }
|
||||
hmac = { version = "0.12.1", features = ["std"] }
|
||||
|
||||
# Personally audited
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { version = "1.0.186", features = ["derive"] }
|
||||
thiserror = "1.0.47"
|
||||
|
||||
# Optional, not personally audited
|
||||
k256 = { workspace = true, default-features = false, features = ["std", "arithmetic"], optional = true }
|
||||
ed25519-dalek = { workspace = true, optional = true }
|
||||
k256 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic"], optional = true }
|
||||
ed25519-dalek = { version = "2.0.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = { workspace = true }
|
||||
keyfork-slip10-test-data = { workspace = true }
|
||||
hex-literal = "0.4.1"
|
||||
keyfork-slip10-test-data = { version = "0.1.0", path = "../../util/keyfork-slip10-test-data" }
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
# Keyfork Derive: BIP-0032 Key Derivation
|
||||
|
||||
Keyfork offers a [BIP-0032] based hierarchial key derivation system enabling
|
||||
the ability to create keys based on a [BIP-0032] seed, a value between 128 to
|
||||
512 bits. The keys can be made using any algorithm supported by Keyfork Derive.
|
||||
Newtypes can be added to wrap around foreign key types that aren't supported by
|
||||
Keyfork.
|
||||
|
||||
Keys derived with the same parameters, from the same seed, will _always_ return
|
||||
the same value. This makes Keyfork a reliable backend for generating encryption
|
||||
or signature keys, as every key can be recovered using the previously used
|
||||
derivation algorithm. However, this may be seen as a concern, as all an
|
||||
attacker may need to recreate all previously-used seeds would be the original
|
||||
derivation seed. For this reason, it is recommended to use the Keyfork server
|
||||
for derivation from the root seed. The Keyfork server will ensure the root seed
|
||||
and any highest-level keys (such as BIP-44, BIP-85, etc.) keys are not leaked.
|
||||
|
||||
The primary use case of Keyfork Derive will be the creation of Derivation
|
||||
Requests, to be used by Keyforkd Client. In the included example, derivation is
|
||||
performed directly on a master seed. This is how Keyforkd works internally.
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
use std::str::FromStr;
|
||||
use keyfork_mnemonic::Mnemonic;
|
||||
use keyfork_derive_util::{*, request::*};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mnemonic = Mnemonic::from_str(
|
||||
"enter settle kiwi high shift absorb protect sword talent museum lazy okay"
|
||||
)?;
|
||||
let path = DerivationPath::from_str("m/44'/0'/0'/0/0")?;
|
||||
|
||||
let request = DerivationRequest::new(
|
||||
DerivationAlgorithm::Secp256k1,
|
||||
&path
|
||||
);
|
||||
|
||||
let key1 = request.derive_with_mnemonic(&mnemonic)?;
|
||||
|
||||
let seed = mnemonic.seed(None)?;
|
||||
let key2 = request.derive_with_master_seed(&seed)?;
|
||||
|
||||
assert_eq!(key1, key2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
[BIP-0032]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
|
@ -1,49 +1,4 @@
|
|||
//! # Extended Key Derivation
|
||||
//!
|
||||
//! Oftentimes, a client will want to create multiple keys. Some examples may include deriving
|
||||
//! non-hardened public keys to see how many wallets have been used, deriving multiple OpenPGP
|
||||
//! keys, or generally avoiding key reuse. While Keyforkd locks the root mnemonic and the
|
||||
//! first-level derivation, any second-level derivations acquired from Keyforkd (for example,
|
||||
//! `"m/44'/0'"`) can be used to derive further keys by converting the key to an Extended Public
|
||||
//! Key or Extended Private Key and calling [`ExtendedPublicKey::derive_child`] or
|
||||
//! [`ExtendedPrivateKey::derive_child`].
|
||||
//!
|
||||
//! # Examples
|
||||
//! ```rust
|
||||
//! use std::str::FromStr;
|
||||
//! use keyfork_mnemonic::Mnemonic;
|
||||
//! use keyfork_derive_util::{*, request::*};
|
||||
//! use k256::SecretKey;
|
||||
//!
|
||||
//! # fn check_wallet<T: PublicKey>(_: ExtendedPublicKey<T>) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
|
||||
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! # let mnemonic = Mnemonic::from_str(
|
||||
//! # "enter settle kiwi high shift absorb protect sword talent museum lazy okay"
|
||||
//! # )?;
|
||||
//! let path = DerivationPath::from_str("m/44'/0'/0'/0")?;
|
||||
//! let request = DerivationRequest::new(
|
||||
//! DerivationAlgorithm::Secp256k1, // The algorithm of k256::SecretKey
|
||||
//! &path,
|
||||
//! );
|
||||
//!
|
||||
//! let response = // perform a Keyforkd Client request...
|
||||
//! # request.derive_with_mnemonic(&mnemonic)?;
|
||||
//! let key: ExtendedPrivateKey<SecretKey> = response.try_into()?;
|
||||
//! let pubkey = key.extended_public_key();
|
||||
//! drop(key);
|
||||
//!
|
||||
//! for account in (0..20).map(|i| DerivationIndex::new(i, false).unwrap()) {
|
||||
//! let derived_key = pubkey.derive_child(&account)?;
|
||||
//! check_wallet(derived_key);
|
||||
//! }
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#[allow(missing_docs)]
|
||||
///
|
||||
pub mod private_key;
|
||||
#[allow(missing_docs)]
|
||||
///
|
||||
pub mod public_key;
|
||||
|
||||
pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey};
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::{DerivationIndex, DerivationPath, ExtendedPublicKey, PrivateKey, PublicKey};
|
||||
|
||||
use keyfork_bug::bug;
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha512;
|
||||
|
@ -12,10 +10,18 @@ const KEY_SIZE: usize = 256;
|
|||
/// Errors associated with creating or deriving Extended Private Keys.
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum Error {
|
||||
/// The seed has an unsuitable length; supported lengths are 16 bytes, 32 bytes, or 64 bytes.
|
||||
#[error("Seed had an unsuitable length: {0}")]
|
||||
BadSeedLength(usize),
|
||||
|
||||
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
|
||||
#[error("Reached maximum depth for key derivation")]
|
||||
Depth,
|
||||
|
||||
/// This should never happen. HMAC keys should be able to take any size input.
|
||||
#[error("Invalid length for HMAC key while generating master key (report me!)")]
|
||||
HmacInvalidLength(#[from] hmac::digest::InvalidLength),
|
||||
|
||||
/// An unknown error occurred while deriving a child key.
|
||||
#[error("Unknown error while deriving child key")]
|
||||
Derivation,
|
||||
|
@ -27,114 +33,23 @@ pub enum Error {
|
|||
/// The given slice was of an inappropriate size to create a Private Key.
|
||||
#[error("The given slice was of an inappropriate size to create a Private Key")]
|
||||
InvalidSliceError(#[from] std::array::TryFromSliceError),
|
||||
|
||||
/// The given data was not a valid key for the chosen key type.
|
||||
#[error("The given data was not a valid key for the chosen key type")]
|
||||
InvalidKey,
|
||||
}
|
||||
|
||||
type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
type ChainCode = [u8; 32];
|
||||
type HmacSha512 = Hmac<Sha512>;
|
||||
|
||||
/// A reference to a variable-length seed. Keyfork automatically supports a seed of 128 bits,
|
||||
/// 256 bits, or 512 bits, but because the master key is derived from a hashed seed, in theory
|
||||
/// any amount of bytes could be used. It is not advised to use a variable-length seed longer
|
||||
/// than 256 bits, as a brute-force attack on the master key could be performed in 2^256
|
||||
/// attempts.
|
||||
///
|
||||
/// Mnemonics use a 512 bit seed, as knowledge of the mnemonics' words (such as through a side
|
||||
/// channel attack) could leak which individual word is used, but not the order the words are
|
||||
/// used in. Using a 512 bit hash to generate the seed results in a more computationally
|
||||
/// expensive brute-force requirement.
|
||||
pub struct VariableLengthSeed<'a> {
|
||||
seed: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> VariableLengthSeed<'a> {
|
||||
/// Create a new VariableLengthSeed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use sha2::{Sha256, Digest};
|
||||
/// use keyfork_derive_util::VariableLengthSeed;
|
||||
///
|
||||
/// let data = b"the missile is very eepy and wants to take a small sleeb";
|
||||
/// let seed = VariableLengthSeed::new(data);
|
||||
/// ```
|
||||
pub fn new(seed: &'a [u8]) -> Self {
|
||||
Self { seed }
|
||||
}
|
||||
}
|
||||
|
||||
mod as_private_key {
|
||||
use super::VariableLengthSeed;
|
||||
|
||||
pub trait AsPrivateKey {
|
||||
fn as_private_key(&self) -> &[u8];
|
||||
}
|
||||
|
||||
impl AsPrivateKey for [u8; 16] {
|
||||
fn as_private_key(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsPrivateKey for [u8; 32] {
|
||||
fn as_private_key(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsPrivateKey for [u8; 64] {
|
||||
fn as_private_key(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsPrivateKey for VariableLengthSeed<'_> {
|
||||
fn as_private_key(&self) -> &[u8] {
|
||||
self.seed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extended private keys derived using BIP-0032.
|
||||
///
|
||||
/// Generic over types implementing [`PrivateKey`].
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ExtendedPrivateKey<K: PrivateKey + Clone> {
|
||||
/// The internal private key data.
|
||||
#[serde(with = "serde_with")]
|
||||
private_key: K,
|
||||
depth: u8,
|
||||
chain_code: ChainCode,
|
||||
}
|
||||
|
||||
mod serde_with {
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn serialize<S, K>(value: &K, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
K: PrivateKey + Clone,
|
||||
{
|
||||
serializer.serialize_bytes(&value.to_bytes())
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D, K>(deserializer: D) -> Result<K, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
K: PrivateKey + Clone,
|
||||
{
|
||||
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
|
||||
let bytes: [u8; 32] = variable_len_bytes.try_into().expect(bug!(
|
||||
"unable to parse serialized private key; no support for static len"
|
||||
));
|
||||
Ok(K::from_bytes(&bytes).expect(bug!("could not deserialize key with invalid scalar")))
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: PrivateKey + Clone> std::fmt::Debug for ExtendedPrivateKey<K> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ExtendedPrivateKey")
|
||||
|
@ -152,54 +67,36 @@ where
|
|||
/// Generate a new [`ExtendedPrivateKey`] from a seed, ideally from a 12-word or 24-word
|
||||
/// mnemonic, but may take 16-byte seeds.
|
||||
///
|
||||
/// # Errors
|
||||
/// The function may return an error if the derived master key could not be parsed as a key for
|
||||
/// the given algorithm (such as exceeding values on the secp256k1 curve).
|
||||
/// # Panics
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// let seed: &[u8; 64] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
|
||||
/// ```
|
||||
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Result<Self> {
|
||||
Self::new_internal(seed.as_private_key())
|
||||
/// The method performs unchecked `try_into()` operations on a constant-sized slice.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned if:
|
||||
/// * The given seed had an incorrect length.
|
||||
/// * A `HmacSha512` can't be constructed - this should be impossible.
|
||||
pub fn new(seed: impl AsRef<[u8]>) -> Result<Self> {
|
||||
Self::new_internal(seed.as_ref())
|
||||
}
|
||||
|
||||
fn new_internal(seed: &[u8]) -> Result<Self> {
|
||||
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
|
||||
.expect(bug!("HmacSha512 InvalidLength should be infallible"))
|
||||
let len = seed.len();
|
||||
if ![16, 32, 64].contains(&len) {
|
||||
return Err(Error::BadSeedLength(len));
|
||||
}
|
||||
|
||||
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())?
|
||||
.chain_update(seed)
|
||||
.finalize()
|
||||
.into_bytes();
|
||||
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
|
||||
|
||||
// Verify the master key is nonzero, hopefully avoiding side-channel attacks.
|
||||
let mut has_any_nonzero = false;
|
||||
// deoptimize arithmetic smartness
|
||||
for byte in private_key.iter().map(std::hint::black_box) {
|
||||
if *byte != 0 {
|
||||
// deoptimize break
|
||||
has_any_nonzero = std::hint::black_box(true);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(has_any_nonzero, bug!("hmac function returned all-zero master key"));
|
||||
|
||||
Self::from_parts(
|
||||
private_key
|
||||
.try_into()
|
||||
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
|
||||
Self::new_from_parts(
|
||||
private_key,
|
||||
0,
|
||||
// Checked: chain_code is always the same length, hash is static size
|
||||
chain_code
|
||||
.try_into()
|
||||
.expect(bug!("Invalid chain code length")),
|
||||
chain_code.try_into().expect("Invalid chain code length"),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -207,151 +104,35 @@ where
|
|||
///
|
||||
/// # Errors
|
||||
/// The function may error if a private key can't be created from the seed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// let key: &[u8; 32] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let chain_code: &[u8; 32] = //
|
||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code);
|
||||
/// ```
|
||||
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
|
||||
match K::from_bytes(key) {
|
||||
Ok(key) => {
|
||||
Ok(Self {
|
||||
private_key: key,
|
||||
depth,
|
||||
chain_code,
|
||||
})
|
||||
}
|
||||
Err(_) => Err(Error::InvalidKey),
|
||||
}
|
||||
pub fn new_from_parts(seed: &[u8], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
|
||||
Ok(Self {
|
||||
private_key: K::from_bytes(seed.try_into()?),
|
||||
depth,
|
||||
chain_code,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`PrivateKey`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # private_key::PrivateKey as _,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let key: &[u8; 32] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let chain_code: &[u8; 32] = //
|
||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
|
||||
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key)?);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn private_key(&self) -> &K {
|
||||
&self.private_key
|
||||
}
|
||||
|
||||
/// Create an [`ExtendedPublicKey`] for the current [`PrivateKey`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # private_key::PrivateKey as _,
|
||||
/// # public_key::PublicKey as _,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let seed: &[u8; 64] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// # let known_key: [u8; 33] = [
|
||||
/// # 0, 242, 26, 9, 159, 68, 199, 0, 206, 71, 248,
|
||||
/// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27,
|
||||
/// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251
|
||||
/// # ];
|
||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
|
||||
/// let xpub = xprv.extended_public_key();
|
||||
/// assert_eq!(known_key, xpub.public_key().to_bytes());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
|
||||
ExtendedPublicKey::from_parts(self.public_key(), self.depth, self.chain_code)
|
||||
ExtendedPublicKey::new(self.public_key(), self.chain_code)
|
||||
}
|
||||
|
||||
/// Return a public key for the current [`PrivateKey`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # private_key::PrivateKey as _,
|
||||
/// # public_key::PublicKey as _,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let seed: &[u8; 64] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
|
||||
/// let pubkey = xprv.public_key();
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn public_key(&self) -> K::PublicKey {
|
||||
self.private_key.public_key()
|
||||
}
|
||||
|
||||
/// Returns the current depth.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let key: &[u8; 32] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let chain_code: &[u8; 32] = //
|
||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
|
||||
/// assert_eq!(xprv.depth(), 4);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn depth(&self) -> u8 {
|
||||
self.depth
|
||||
}
|
||||
|
||||
/// Returns a copy of the current chain code.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let key: &[u8; 32] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let chain_code: &[u8; 32] = //
|
||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
|
||||
/// assert_eq!(chain_code, &xprv.chain_code());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn chain_code(&self) -> [u8; 32] {
|
||||
self.chain_code
|
||||
}
|
||||
|
@ -359,29 +140,9 @@ where
|
|||
/// Derive a child using the given [`DerivationPath`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned under the same circumstances as
|
||||
/// [`ExtendedPrivateKey::derive_child`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let seed: &[u8; 64] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
|
||||
/// let path = DerivationPath::default()
|
||||
/// .chain_push(DerivationIndex::new(44, true)?)
|
||||
/// .chain_push(DerivationIndex::new(0, true)?)
|
||||
/// .chain_push(DerivationIndex::new(0, true)?)
|
||||
/// .chain_push(DerivationIndex::new(0, false)?);
|
||||
/// let derived_xprv = root_xprv.derive_path(&path)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn derive_path(&self, path: &DerivationPath) -> Result<Self> {
|
||||
if path.path.is_empty() {
|
||||
Ok(self.clone())
|
||||
|
@ -404,38 +165,11 @@ where
|
|||
/// * The depth exceeds the maximum depth [`u8::MAX`].
|
||||
/// * A `HmacSha512` can't be constructed - this should be impossible.
|
||||
/// * Deriving a child key fails. Check the documentation for your [`PrivateKey`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn check_empty(p: &ExtendedPrivateKey<PrivateKey>) -> Result<(), std::io::Error> {
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let seed: &[u8; 64] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
|
||||
/// let bip44_wallet = DerivationPath::default()
|
||||
/// .chain_push(DerivationIndex::new(44, true)?)
|
||||
/// .chain_push(DerivationIndex::new(0, true)?)
|
||||
/// .chain_push(DerivationIndex::new(0, true)?)
|
||||
/// .chain_push(DerivationIndex::new(0, false)?);
|
||||
/// let change_xprv = root_xprv.derive_path(&bip44_wallet)?;
|
||||
/// for account in (0..20).map(|i| DerivationIndex::new(i, false).unwrap()) {
|
||||
/// let account_xprv = change_xprv.derive_child(&account)?;
|
||||
/// check_empty(&account_xprv)?;
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> {
|
||||
let depth = self.depth.checked_add(1).ok_or(Error::Depth)?;
|
||||
|
||||
let mut hmac = HmacSha512::new_from_slice(&self.chain_code)
|
||||
.expect(bug!("HmacSha512 InvalidLength should be infallible"));
|
||||
let mut hmac =
|
||||
HmacSha512::new_from_slice(&self.chain_code).map_err(Error::HmacInvalidLength)?;
|
||||
if index.is_hardened() {
|
||||
hmac.update(&[0]);
|
||||
hmac.update(&self.private_key.to_bytes());
|
||||
|
@ -453,7 +187,7 @@ where
|
|||
.derive_child(
|
||||
&private_key
|
||||
.try_into()
|
||||
.expect(bug!("Invalid length for private key")),
|
||||
.expect("Invalid length for private key"),
|
||||
)
|
||||
.map_err(|_| Error::Derivation)?;
|
||||
|
||||
|
@ -462,7 +196,7 @@ where
|
|||
depth,
|
||||
chain_code: chain_code
|
||||
.try_into()
|
||||
.expect(bug!("Invalid length for chain code")),
|
||||
.expect("Invalid length for chain code"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,15 +4,13 @@ use hmac::{Hmac, Mac};
|
|||
use sha2::Sha512;
|
||||
use thiserror::Error;
|
||||
|
||||
use keyfork_bug::bug;
|
||||
|
||||
const KEY_SIZE: usize = 256;
|
||||
|
||||
/// Errors associated with creating or deriving Extended Public Keys.
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum Error {
|
||||
/// BIP-0032 does not support hardened public key derivation from parent public keys.
|
||||
#[error("Hardened child public keys may not be derived from parent public keys")]
|
||||
/// BIP-0032 does not support deriving public keys from hardened private keys.
|
||||
#[error("Public keys may not be derived when hardened")]
|
||||
HardenedIndex,
|
||||
|
||||
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
|
||||
|
@ -46,51 +44,15 @@ where
|
|||
K: PublicKey,
|
||||
{
|
||||
/// Create a new [`ExtendedPublicKey`] from previously known values.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # public_key::PublicKey as _,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let key: &[u8; 33] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let chain_code: &[u8; 32] = //
|
||||
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
/// let pubkey = PublicKey::from_bytes(key);
|
||||
/// let xpub = ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
|
||||
pub fn new(public_key: K, chain_code: ChainCode) -> Self {
|
||||
Self {
|
||||
public_key,
|
||||
depth,
|
||||
depth: 0,
|
||||
chain_code,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the internal [`PublicKey`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # public_key::PublicKey as _,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let key: &[u8; 33] = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
/// # let pubkey = PublicKey::from_bytes(key);
|
||||
/// let xpub = //
|
||||
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
|
||||
/// let pubkey = xpub.public_key();
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn public_key(&self) -> &K {
|
||||
&self.public_key
|
||||
}
|
||||
|
@ -108,25 +70,6 @@ where
|
|||
/// * The depth exceeds the maximum depth [`u8::MAX`].
|
||||
/// * A `HmacSha512` can't be constructed - this should be impossible.
|
||||
/// * Deriving a child key fails. Check the documentation for your [`PublicKey`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # public_key::PublicKey as _,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let key: &[u8; 33] = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
/// # let pubkey = PublicKey::from_bytes(key);
|
||||
/// let xpub = //
|
||||
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
|
||||
/// let index = DerivationIndex::new(0, false)?;
|
||||
/// let child = xpub.derive_child(&index)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> {
|
||||
if index.is_hardened() {
|
||||
return Err(Error::HardenedIndex);
|
||||
|
@ -144,11 +87,9 @@ where
|
|||
let (child_key, chain_code) = hmac.split_at(KEY_SIZE / 8);
|
||||
let derived_key = self
|
||||
.public_key
|
||||
.derive_child(child_key.try_into().expect(bug!("Invalid key length")))
|
||||
.derive_child(child_key.try_into().expect("Invalid key length"))
|
||||
.map_err(|_| Error::Derivation)?;
|
||||
let chain_code = chain_code
|
||||
.try_into()
|
||||
.expect(bug!("Invalid chain code length"));
|
||||
let chain_code = chain_code.try_into().expect("Invalid chain code length");
|
||||
|
||||
Ok(Self {
|
||||
public_key: derived_key,
|
||||
|
|
|
@ -23,20 +23,8 @@ impl DerivationIndex {
|
|||
/// Creates a new [`DerivationIndex`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the index is larger than the hardened flag.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::*;
|
||||
/// let bip44 = DerivationIndex::new(44, true).unwrap();
|
||||
/// ```
|
||||
///
|
||||
/// Using a derivation index that is higher than 2^31 returns an error:
|
||||
///
|
||||
/// ```rust,should_panic
|
||||
/// # use keyfork_derive_util::*;
|
||||
/// let too_high = DerivationIndex::new(u32::MAX, true).unwrap();
|
||||
/// ```
|
||||
pub const fn new(index: u32, hardened: bool) -> Result<Self> {
|
||||
if index & (0b1 << 31) > 0 {
|
||||
return Err(Error::IndexTooLarge(index));
|
||||
|
@ -58,13 +46,6 @@ impl DerivationIndex {
|
|||
|
||||
/// Return the internal derivation index. Note that if the derivation index is hardened, the
|
||||
/// highest bit will be set, and the value can't be used to create a new derivation index.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::*;
|
||||
/// assert_eq!(DerivationIndex::new(44, true).unwrap().inner(), 2147483692);
|
||||
/// assert_eq!(DerivationIndex::new(200, false).unwrap().inner(), 200);
|
||||
/// ```
|
||||
pub fn inner(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
@ -73,15 +54,7 @@ impl DerivationIndex {
|
|||
self.0.to_be_bytes()
|
||||
}
|
||||
|
||||
/// Whether or not the index is hardened, allowing deriving the key from a known parent public
|
||||
/// key.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::*;
|
||||
/// assert_eq!(DerivationIndex::new(0, true).unwrap().is_hardened(), true);
|
||||
/// assert_eq!(DerivationIndex::new(0, false).unwrap().is_hardened(), false);
|
||||
/// ```
|
||||
/// Whether or not the index is hardened, allowing deriving the key from a known parent key.
|
||||
pub fn is_hardened(&self) -> bool {
|
||||
self.0 & (0b1 << 31) != 0
|
||||
}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
//! BIP-0032 derivation utilities.
|
||||
|
||||
///
|
||||
pub mod extended_key;
|
||||
pub mod request;
|
||||
|
||||
mod index;
|
||||
mod path;
|
||||
|
||||
#[doc(hidden)]
|
||||
///
|
||||
pub mod index;
|
||||
///
|
||||
pub mod path;
|
||||
///
|
||||
pub mod private_key;
|
||||
|
||||
#[doc(hidden)]
|
||||
///
|
||||
pub mod public_key;
|
||||
///
|
||||
pub mod request;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::extended_key::{private_key::{ExtendedPrivateKey, Error as XPrvError, VariableLengthSeed}, public_key::{ExtendedPublicKey, Error as XPubError}};
|
||||
|
||||
pub use crate::{
|
||||
index::{DerivationIndex, Error as IndexError},
|
||||
path::{DerivationPath, Error as PathError},
|
||||
private_key::{PrivateKey, PrivateKeyError},
|
||||
public_key::{PublicKey, PublicKeyError},
|
||||
extended_key::{private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey},
|
||||
index::DerivationIndex,
|
||||
path::DerivationPath,
|
||||
private_key::PrivateKey,
|
||||
public_key::PublicKey,
|
||||
};
|
||||
|
|
|
@ -51,30 +51,7 @@ impl DerivationPath {
|
|||
self.path.push(index);
|
||||
}
|
||||
|
||||
/// Return the inner path.
|
||||
pub fn inner(&self) -> &Vec<DerivationIndex> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Append an index to the path, returning self to allow chaining method calls.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::*;
|
||||
/// # fn discover_wallet(_p: DerivationPath) -> Result<bool, std::io::Error> { Ok(true) }
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let account = 0;
|
||||
/// let path = DerivationPath::default()
|
||||
/// .chain_push(DerivationIndex::new(44, true)?)
|
||||
/// .chain_push(DerivationIndex::new(0, true)?)
|
||||
/// .chain_push(DerivationIndex::new(account, true)?);
|
||||
/// let mut has_wallet = false;
|
||||
/// for index in (0..20).map(|i| DerivationIndex::new(i, true).unwrap()) {
|
||||
/// has_wallet = has_wallet || discover_wallet(path.clone().chain_push(index))?;
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn chain_push(mut self, index: DerivationIndex) -> Self {
|
||||
self.path.push(index);
|
||||
self
|
||||
|
|
|
@ -13,32 +13,9 @@ pub trait PrivateKey: Sized {
|
|||
type Err: std::error::Error;
|
||||
|
||||
/// Create a Self from bytes.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # private_key::TestPrivateKey as OurPrivateKey,
|
||||
/// # };
|
||||
/// let key_data: &[u8; 32] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let private_key = OurPrivateKey::from_bytes(key_data);
|
||||
/// ```
|
||||
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err>;
|
||||
fn from_bytes(b: &PrivateKeyBytes) -> Self;
|
||||
|
||||
/// Convert a &Self to bytes.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # private_key::TestPrivateKey as OurPrivateKey,
|
||||
/// # };
|
||||
/// let key_data: &[u8; 32] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
|
||||
/// assert_eq!(key_data, &private_key.to_bytes());
|
||||
/// ```
|
||||
fn to_bytes(&self) -> PrivateKeyBytes;
|
||||
|
||||
/*
|
||||
|
@ -50,40 +27,17 @@ pub trait PrivateKey: Sized {
|
|||
*/
|
||||
|
||||
/// The initial key for BIP-0032 and SLIP-0010 derivation, such as secp256k1's "Bitcoin seed".
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # private_key::TestPrivateKey as OurPrivateKey,
|
||||
/// # };
|
||||
/// assert_eq!(OurPrivateKey::key(), "testing seed");
|
||||
/// ```
|
||||
fn key() -> &'static str;
|
||||
|
||||
/// Generate a [`Self::PublicKey`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # private_key::TestPrivateKey as OurPrivateKey,
|
||||
/// # };
|
||||
/// let key_data: &[u8; 32] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
|
||||
/// let public_key = private_key.public_key();
|
||||
/// ```
|
||||
fn public_key(&self) -> Self::PublicKey;
|
||||
|
||||
/// Derive a child [`PrivateKey`] with given `PrivateKeyBytes`. The implementation of
|
||||
/// derivation is algorithm-specific and a specification should be consulted when implementing
|
||||
/// this method.
|
||||
/// Derive a child [`PrivateKey`] with given `PrivateKeyBytes`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned if:
|
||||
/// * An all-zero `other` is provided.
|
||||
/// * A nonzero `other` is provided.
|
||||
/// * An error specific to the given algorithm was encountered.
|
||||
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
|
||||
|
||||
|
@ -100,10 +54,6 @@ pub enum PrivateKeyError {
|
|||
/// For the given algorithm, the private key must be nonzero.
|
||||
#[error("The provided private key must be nonzero, but is not")]
|
||||
NonZero,
|
||||
|
||||
/// A scalar could not be constructed for the given algorithm.
|
||||
#[error("A scalar could not be constructed for the given algorithm")]
|
||||
InvalidScalar,
|
||||
}
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
|
@ -118,8 +68,8 @@ impl PrivateKey for k256::SecretKey {
|
|||
"Bitcoin seed"
|
||||
}
|
||||
|
||||
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
Self::from_slice(b).map_err(|_| PrivateKeyError::InvalidScalar)
|
||||
fn from_bytes(b: &PrivateKeyBytes) -> Self {
|
||||
Self::from_slice(b).expect("Invalid private key bytes")
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> PrivateKeyBytes {
|
||||
|
@ -132,19 +82,20 @@ impl PrivateKey for k256::SecretKey {
|
|||
}
|
||||
|
||||
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
use k256::elliptic_curve::ScalarPrimitive;
|
||||
use k256::{Scalar, Secp256k1};
|
||||
|
||||
// Construct a scalar from bytes
|
||||
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(other.into());
|
||||
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
|
||||
let scalar = scalar.ok_or(PrivateKeyError::InvalidScalar)?;
|
||||
let scalar = Scalar::from(scalar);
|
||||
if other.iter().all(|n| n == &0) {
|
||||
return Err(PrivateKeyError::NonZero);
|
||||
}
|
||||
let other = *other;
|
||||
// Checked: See above nonzero check
|
||||
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
|
||||
.expect("Should have been able to get a NonZeroScalar");
|
||||
|
||||
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
|
||||
let nonzero_scalar = Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
|
||||
.ok_or(PrivateKeyError::NonZero)?;
|
||||
Ok(Self::from(nonzero_scalar))
|
||||
Ok(
|
||||
Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
|
||||
.map(Into::into)
|
||||
.expect("Should be able to make Key"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,8 +108,8 @@ impl PrivateKey for ed25519_dalek::SigningKey {
|
|||
"ed25519 seed"
|
||||
}
|
||||
|
||||
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
Ok(Self::from_bytes(b))
|
||||
fn from_bytes(b: &PrivateKeyBytes) -> Self {
|
||||
Self::from_bytes(b)
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> PrivateKeyBytes {
|
||||
|
@ -178,47 +129,3 @@ impl PrivateKey for ed25519_dalek::SigningKey {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
use crate::public_key::TestPublicKey;
|
||||
|
||||
/// A private key that can be used for testing purposes. Does not utilize any significant
|
||||
/// cryptographic operations.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TestPrivateKey {
|
||||
key: [u8; 32],
|
||||
}
|
||||
|
||||
impl TestPrivateKey {
|
||||
pub(crate) fn public_key(&self) -> TestPublicKey {
|
||||
let mut bytes = [0u8; 33];
|
||||
for (i, byte) in self.key.iter().enumerate() {
|
||||
bytes[i + 1] = byte ^ 0xFF;
|
||||
}
|
||||
TestPublicKey { key: bytes }
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateKey for TestPrivateKey {
|
||||
type PublicKey = TestPublicKey;
|
||||
type Err = PrivateKeyError;
|
||||
|
||||
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
Ok(Self { key: *b })
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> PrivateKeyBytes {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn key() -> &'static str {
|
||||
"testing seed"
|
||||
}
|
||||
|
||||
fn public_key(&self) -> Self::PublicKey {
|
||||
self.public_key()
|
||||
}
|
||||
|
||||
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
Ok(Self { key: *other })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@ use ripemd::Ripemd160;
|
|||
use sha2::Sha256;
|
||||
use thiserror::Error;
|
||||
|
||||
use keyfork_bug::bug;
|
||||
|
||||
pub(crate) type PublicKeyBytes = [u8; 33];
|
||||
|
||||
/// Functions required to use an `ExtendedPublicKey`.
|
||||
|
@ -21,51 +19,25 @@ pub trait PublicKey: Sized {
|
|||
*/
|
||||
|
||||
/// Convert a &Self to bytes.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # private_key::TestPrivateKey as OurPrivateKey,
|
||||
/// # };
|
||||
/// let key_data: &[u8; 32] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
|
||||
/// let public_key_bytes = private_key.public_key().to_bytes();
|
||||
/// ```
|
||||
fn to_bytes(&self) -> PublicKeyBytes;
|
||||
|
||||
/// Derive a child [`PublicKey`] with given `PrivateKeyBytes`. The implementation of
|
||||
/// derivation is algorithm-specific and a specification should be consulted when implementing
|
||||
/// this method.
|
||||
/// Derive a child [`PublicKey`] with given `PrivateKeyBytes`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned if:
|
||||
/// * An all-zero `other` is provided.
|
||||
/// * A nonzero `other` is provided.
|
||||
/// * An error specific to the given algorithm was encountered.
|
||||
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
|
||||
|
||||
/// Create a BIP-0032/SLIP-0010 fingerprint from the public key.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # private_key::TestPrivateKey as OurPrivateKey,
|
||||
/// # };
|
||||
/// let key_data: &[u8; 32] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
|
||||
/// let fingerprint = private_key.public_key().fingerprint();
|
||||
/// ```
|
||||
fn fingerprint(&self) -> [u8; 4] {
|
||||
let hash = Sha256::new().chain_update(self.to_bytes()).finalize();
|
||||
let hash = Ripemd160::new().chain_update(hash).finalize();
|
||||
// Note: Safety assured by type returned from Ripemd160
|
||||
hash[..4]
|
||||
.try_into()
|
||||
.expect(bug!("Ripemd160 returned too little data"))
|
||||
.expect("Ripemd160 returned too little data")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,10 +49,6 @@ pub enum PublicKeyError {
|
|||
#[error("The provided public key must be nonzero, but is not")]
|
||||
NonZero,
|
||||
|
||||
/// A scalar could not be constructed for the given algorithm.
|
||||
#[error("A scalar could not be constructed for the given algorithm")]
|
||||
InvalidScalar,
|
||||
|
||||
/// Public key derivation is unsupported for this algorithm.
|
||||
#[error("Public key derivation is unsupported for this algorithm")]
|
||||
DerivationUnsupported,
|
||||
|
@ -89,7 +57,7 @@ pub enum PublicKeyError {
|
|||
#[cfg(feature = "secp256k1")]
|
||||
use k256::{
|
||||
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
|
||||
AffinePoint,
|
||||
AffinePoint, NonZeroScalar,
|
||||
};
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
|
@ -109,18 +77,15 @@ impl PublicKey for k256::PublicKey {
|
|||
}
|
||||
|
||||
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
use k256::elliptic_curve::ScalarPrimitive;
|
||||
use k256::{Secp256k1, Scalar};
|
||||
if other.iter().all(|n| n == &0) {
|
||||
return Err(PublicKeyError::NonZero);
|
||||
}
|
||||
// Checked: See above
|
||||
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
|
||||
.expect("Should have been able to get a NonZeroScalar");
|
||||
|
||||
// Construct a scalar from bytes
|
||||
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(&other.into());
|
||||
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
|
||||
let scalar = scalar.ok_or(PublicKeyError::InvalidScalar)?;
|
||||
let scalar = Scalar::from(scalar);
|
||||
|
||||
let point = self.to_projective() + (AffinePoint::generator() * scalar);
|
||||
Ok(Self::from_affine(point.into())
|
||||
.expect(bug!("Could not from_affine after scalar arithmetic")))
|
||||
let point = self.to_projective() + (AffinePoint::generator() * *scalar);
|
||||
Ok(Self::from_affine(point.into()).expect("Could not from_affine after scalar arithmetic"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,35 +112,3 @@ impl PublicKey for VerifyingKey {
|
|||
Err(Self::Err::DerivationUnsupported)
|
||||
}
|
||||
}
|
||||
|
||||
/// A public key that can be used for testing purposes. Does not utilize any significant
|
||||
/// cryptographic operations.
|
||||
#[derive(Clone)]
|
||||
pub struct TestPublicKey {
|
||||
pub(crate) key: [u8; 33],
|
||||
}
|
||||
|
||||
impl TestPublicKey {
|
||||
/// Create a new TestPublicKey from the given bytes.
|
||||
#[allow(dead_code)]
|
||||
pub fn from_bytes(b: &[u8]) -> Self {
|
||||
Self {
|
||||
key: b
|
||||
.try_into()
|
||||
.expect(bug!("invalid size when constructing TestPublicKey")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey for TestPublicKey {
|
||||
type Err = PublicKeyError;
|
||||
|
||||
fn to_bytes(&self) -> PublicKeyBytes {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn derive_child(&self, _other: PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
// whatever it takes for tests to pass...
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,10 @@
|
|||
// Because all algorithms make use of wildcard matching
|
||||
#![allow(clippy::match_wildcard_for_single_variants)]
|
||||
|
||||
//! # Derivation Requests
|
||||
//!
|
||||
//! Derivation requests can be sent to Keyforkd using Keyforkd Client to request derivation from a
|
||||
//! mnemonic or seed that has been loaded into Keyforkd.
|
||||
//!
|
||||
//! # Examples
|
||||
//! ```rust
|
||||
//! use std::str::FromStr;
|
||||
//! use keyfork_derive_util::{DerivationPath, request::{DerivationRequest, DerivationAlgorithm}};
|
||||
//!
|
||||
//! let path = DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap();
|
||||
//! let request = DerivationRequest::new(
|
||||
//! DerivationAlgorithm::Secp256k1,
|
||||
//! &path
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
extended_key::private_key::{Error as XPrvError, VariableLengthSeed},
|
||||
private_key::{PrivateKey, TestPrivateKey},
|
||||
DerivationPath, ExtendedPrivateKey,
|
||||
extended_key::private_key::Error as XPrvError, DerivationPath, ExtendedPrivateKey, PrivateKey,
|
||||
};
|
||||
|
||||
use keyfork_mnemonic::{Mnemonic, MnemonicGenerationError};
|
||||
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An error encountered while deriving a key.
|
||||
|
@ -49,15 +29,12 @@ pub type Result<T, E = DerivationError> = std::result::Result<T, E>;
|
|||
|
||||
/// The algorithm to derive a key for. The choice of algorithm will result in a different resulting
|
||||
/// derivation.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum DerivationAlgorithm {
|
||||
#[allow(missing_docs)]
|
||||
Ed25519,
|
||||
#[allow(missing_docs)]
|
||||
Secp256k1,
|
||||
#[doc(hidden)]
|
||||
TestAlgorithm,
|
||||
}
|
||||
|
||||
impl DerivationAlgorithm {
|
||||
|
@ -65,8 +42,7 @@ impl DerivationAlgorithm {
|
|||
///
|
||||
/// # Errors
|
||||
/// The method may error if the derivation fails or if the algorithm is not supported.
|
||||
fn derive(&self, seed: &[u8], path: &DerivationPath) -> Result<DerivationResponse> {
|
||||
let seed = VariableLengthSeed::new(seed);
|
||||
pub fn derive(&self, seed: Vec<u8>, path: &DerivationPath) -> Result<DerivationResponse> {
|
||||
match self {
|
||||
#[cfg(feature = "ed25519")]
|
||||
Self::Ed25519 => {
|
||||
|
@ -86,14 +62,6 @@ impl DerivationAlgorithm {
|
|||
&derived_key,
|
||||
))
|
||||
}
|
||||
Self::TestAlgorithm => {
|
||||
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed)?;
|
||||
let derived_key = key.derive_path(path)?;
|
||||
Ok(DerivationResponse::with_algo_and_xprv(
|
||||
self.clone(),
|
||||
&derived_key,
|
||||
))
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(DerivationError::Algorithm),
|
||||
}
|
||||
|
@ -112,20 +80,8 @@ impl std::str::FromStr for DerivationAlgorithm {
|
|||
}
|
||||
}
|
||||
|
||||
/// Acquire the associated [`DerivationAlgorithm`] for a [`PrivateKey`].
|
||||
pub trait AsAlgorithm: PrivateKey {
|
||||
/// Return the appropriate [`DerivationAlgorithm`].
|
||||
fn as_algorithm() -> DerivationAlgorithm;
|
||||
}
|
||||
|
||||
impl AsAlgorithm for TestPrivateKey {
|
||||
fn as_algorithm() -> DerivationAlgorithm {
|
||||
DerivationAlgorithm::TestAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
/// A derivation request.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DerivationRequest {
|
||||
algorithm: DerivationAlgorithm,
|
||||
path: DerivationPath,
|
||||
|
@ -133,23 +89,6 @@ pub struct DerivationRequest {
|
|||
|
||||
impl DerivationRequest {
|
||||
/// Create a new derivation request.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # request::*,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let algo: DerivationAlgorithm = //
|
||||
/// # DerivationAlgorithm::TestAlgorithm;
|
||||
/// let path: DerivationPath = //
|
||||
/// # DerivationPath::default();
|
||||
/// let request = DerivationRequest::new(algo, &path);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
pub fn new(algorithm: DerivationAlgorithm, path: &DerivationPath) -> Self {
|
||||
Self {
|
||||
algorithm,
|
||||
|
@ -158,24 +97,6 @@ impl DerivationRequest {
|
|||
}
|
||||
|
||||
/// Return the path of the derivation request.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # request::*,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let algo: DerivationAlgorithm = //
|
||||
/// # DerivationAlgorithm::TestAlgorithm;
|
||||
/// let path: DerivationPath = //
|
||||
/// # DerivationPath::default();
|
||||
/// let request = DerivationRequest::new(algo, &path);
|
||||
/// assert_eq!(&path, request.path());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
pub fn path(&self) -> &DerivationPath {
|
||||
&self.path
|
||||
}
|
||||
|
@ -184,70 +105,28 @@ impl DerivationRequest {
|
|||
///
|
||||
/// # Errors
|
||||
/// The method may error if the derivation fails or if the algorithm is not supported.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # request::*,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mnemonic: keyfork_mnemonic::Mnemonic = //
|
||||
/// # keyfork_mnemonic::Mnemonic::from_entropy(
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
/// # )?;
|
||||
/// let algo: DerivationAlgorithm = //
|
||||
/// # DerivationAlgorithm::TestAlgorithm;
|
||||
/// let path: DerivationPath = //
|
||||
/// # DerivationPath::default();
|
||||
/// let request = DerivationRequest::new(algo, &path);
|
||||
/// let response = request.derive_with_mnemonic(&mnemonic)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
|
||||
// TODO: passphrase support and/or store passphrase within mnemonic
|
||||
self.derive_with_master_seed(&mnemonic.generate_seed(None))
|
||||
self.derive_with_master_seed(mnemonic.seed(None)?)
|
||||
}
|
||||
|
||||
/// Derive an [`ExtendedPrivateKey`] using the given seed.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may error if the derivation fails or if the algorithm is not supported.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use keyfork_derive_util::{
|
||||
/// # *,
|
||||
/// # request::*,
|
||||
/// # public_key::TestPublicKey as PublicKey,
|
||||
/// # private_key::TestPrivateKey as PrivateKey,
|
||||
/// # };
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let seed: &[u8; 64] = //
|
||||
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let algo: DerivationAlgorithm = //
|
||||
/// # DerivationAlgorithm::TestAlgorithm;
|
||||
/// let path: DerivationPath = //
|
||||
/// # DerivationPath::default();
|
||||
/// let request = DerivationRequest::new(algo, &path);
|
||||
/// let response = request.derive_with_master_seed(seed)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
pub fn derive_with_master_seed(&self, seed: &[u8]) -> Result<DerivationResponse> {
|
||||
pub fn derive_with_master_seed(&self, seed: Vec<u8>) -> Result<DerivationResponse> {
|
||||
self.algorithm.derive(seed, &self.path)
|
||||
}
|
||||
}
|
||||
|
||||
/// A response to a [`DerivationRequest`]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DerivationResponse {
|
||||
/// The algorithm used to derive the data.
|
||||
pub algorithm: DerivationAlgorithm,
|
||||
|
||||
/// The derived private key.
|
||||
pub data: [u8; 32],
|
||||
pub data: Vec<u8>,
|
||||
|
||||
/// The chain code, used for further derivation.
|
||||
pub chain_code: [u8; 32],
|
||||
|
@ -258,13 +137,13 @@ pub struct DerivationResponse {
|
|||
|
||||
impl DerivationResponse {
|
||||
/// Create a [`DerivationResponse`] with the given values.
|
||||
fn with_algo_and_xprv<T: PrivateKey + Clone>(
|
||||
pub fn with_algo_and_xprv<T: PrivateKey + Clone>(
|
||||
algorithm: DerivationAlgorithm,
|
||||
xprv: &ExtendedPrivateKey<T>,
|
||||
) -> Self {
|
||||
Self {
|
||||
algorithm,
|
||||
data: PrivateKey::to_bytes(xprv.private_key()),
|
||||
data: PrivateKey::to_bytes(xprv.private_key()).to_vec(),
|
||||
chain_code: xprv.chain_code(),
|
||||
depth: xprv.depth(),
|
||||
}
|
||||
|
@ -285,67 +164,47 @@ pub enum TryFromDerivationResponseError {
|
|||
}
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
mod secp256k1 {
|
||||
use super::*;
|
||||
use k256::SecretKey;
|
||||
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
impl AsAlgorithm for SecretKey {
|
||||
fn as_algorithm() -> DerivationAlgorithm {
|
||||
DerivationAlgorithm::Secp256k1
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<SecretKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
||||
match value.algorithm {
|
||||
DerivationAlgorithm::Secp256k1 => {
|
||||
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into)
|
||||
}
|
||||
_ => Err(Self::Error::Algorithm),
|
||||
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> {
|
||||
match value.algorithm {
|
||||
DerivationAlgorithm::Secp256k1 => {
|
||||
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from)
|
||||
}
|
||||
_ => Err(Self::Error::Algorithm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<SecretKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
#[cfg(feature = "secp256k1")]
|
||||
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
fn try_from(value: DerivationResponse) -> Result<Self, Self::Error> {
|
||||
ExtendedPrivateKey::<SecretKey>::try_from(&value)
|
||||
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> {
|
||||
ExtendedPrivateKey::<k256::SecretKey>::try_from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ed25519")]
|
||||
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> {
|
||||
match value.algorithm {
|
||||
DerivationAlgorithm::Ed25519 => {
|
||||
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from)
|
||||
}
|
||||
_ => Err(Self::Error::Algorithm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ed25519")]
|
||||
mod ed25519 {
|
||||
use super::*;
|
||||
use ed25519_dalek::SigningKey;
|
||||
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
impl AsAlgorithm for SigningKey {
|
||||
fn as_algorithm() -> DerivationAlgorithm {
|
||||
DerivationAlgorithm::Ed25519
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<SigningKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
|
||||
match value.algorithm {
|
||||
DerivationAlgorithm::Ed25519 => {
|
||||
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into)
|
||||
}
|
||||
_ => Err(Self::Error::Algorithm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<SigningKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
fn try_from(value: DerivationResponse) -> Result<Self, Self::Error> {
|
||||
ExtendedPrivateKey::<SigningKey>::try_from(&value)
|
||||
}
|
||||
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> {
|
||||
ExtendedPrivateKey::<ed25519_dalek::SigningKey>::try_from(&value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ fn secp256k1() {
|
|||
|
||||
let tests = test_data()
|
||||
.unwrap()
|
||||
.remove("secp256k1")
|
||||
.remove(&"secp256k1".to_string())
|
||||
.unwrap();
|
||||
|
||||
for per_seed in tests {
|
||||
|
@ -30,8 +30,7 @@ fn secp256k1() {
|
|||
} = test;
|
||||
|
||||
// Tests for ExtendedPrivateKey
|
||||
let varlen_seed = VariableLengthSeed::new(seed);
|
||||
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed).unwrap();
|
||||
let xkey = ExtendedPrivateKey::<SecretKey>::new(seed).unwrap();
|
||||
let derived_key = xkey.derive_path(&chain).unwrap();
|
||||
assert_eq!(
|
||||
derived_key.chain_code().as_slice(),
|
||||
|
@ -51,8 +50,8 @@ fn secp256k1() {
|
|||
|
||||
// Tests for DerivationRequest
|
||||
let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
|
||||
let response = request.derive_with_master_seed(seed).unwrap();
|
||||
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
|
||||
let response = request.derive_with_master_seed(seed.clone()).unwrap();
|
||||
assert_eq!(&response.data, private_key, "test: {chain}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +61,7 @@ fn secp256k1() {
|
|||
fn ed25519() {
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
||||
let tests = test_data().unwrap().remove("ed25519").unwrap();
|
||||
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
|
||||
|
||||
for per_seed in tests {
|
||||
let seed = &per_seed.seed;
|
||||
|
@ -76,8 +75,7 @@ fn ed25519() {
|
|||
} = test;
|
||||
|
||||
// Tests for ExtendedPrivateKey
|
||||
let varlen_seed = VariableLengthSeed::new(seed);
|
||||
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed).unwrap();
|
||||
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
|
||||
let derived_key = xkey.derive_path(&chain).unwrap();
|
||||
assert_eq!(
|
||||
derived_key.chain_code().as_slice(),
|
||||
|
@ -97,8 +95,8 @@ fn ed25519() {
|
|||
|
||||
// Tests for DerivationRequest
|
||||
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain);
|
||||
let response = request.derive_with_master_seed(seed).unwrap();
|
||||
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
|
||||
let response = request.derive_with_master_seed(seed.to_vec()).unwrap();
|
||||
assert_eq!(&response.data, private_key, "test: {chain}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,38 @@
|
|||
[package]
|
||||
name = "keyfork-shard"
|
||||
version = "0.2.3"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["openpgp", "openpgp-card", "qrcode", "bin"]
|
||||
bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
|
||||
default = ["openpgp", "openpgp-card", "qrcode"]
|
||||
openpgp = ["sequoia-openpgp", "anyhow"]
|
||||
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"]
|
||||
qrcode = ["keyfork-qrcode"]
|
||||
bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-bug = { workspace = true }
|
||||
keyfork-prompt = { workspace = true, default-features = false, features = ["mnemonic"] }
|
||||
keyfork-qrcode = { workspace = true, optional = true, default-features = false }
|
||||
smex = { workspace = true }
|
||||
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] }
|
||||
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", optional = true }
|
||||
smex = { version = "0.1.0", path = "../util/smex" }
|
||||
|
||||
sharks = "0.5.0"
|
||||
thiserror = { workspace = true }
|
||||
thiserror = "1.0.50"
|
||||
|
||||
# Remote operator mode
|
||||
keyfork-mnemonic = { workspace = true }
|
||||
keyfork-mnemonic-util = { version = "0.1.0", path = "../util/keyfork-mnemonic-util" }
|
||||
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
hkdf = { version = "0.12.4", features = ["std"] }
|
||||
sha2 = { workspace = true }
|
||||
sha2 = "0.10.8"
|
||||
|
||||
# OpenPGP
|
||||
keyfork-derive-openpgp = { workspace = true, default-features = false }
|
||||
anyhow = { workspace = true, optional = true }
|
||||
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp" }
|
||||
anyhow = { version = "1.0.79", optional = true }
|
||||
card-backend = { version = "0.2.0", optional = true }
|
||||
card-backend-pcsc = { workspace = true, optional = true }
|
||||
openpgp-card-sequoia = { workspace = true, optional = true }
|
||||
openpgp-card = { workspace = true, optional = true }
|
||||
sequoia-openpgp = { workspace = true, optional = true }
|
||||
base64 = "0.22.0"
|
||||
card-backend-pcsc = { version = "0.5.0", optional = true }
|
||||
openpgp-card-sequoia = { version = "0.2.0", optional = true, default-features = false }
|
||||
openpgp-card = { version = "0.4.0", optional = true }
|
||||
sequoia-openpgp = { version = "1.17.0", optional = true, default-features = false }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! Combine OpenPGP shards and output the hex-encoded secret.
|
||||
//!
|
||||
|
||||
use std::{
|
||||
env,
|
||||
|
@ -7,36 +7,52 @@ use std::{
|
|||
process::ExitCode,
|
||||
};
|
||||
|
||||
use keyfork_prompt::{DefaultTerminal, default_terminal};
|
||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};
|
||||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
||||
fn validate(
|
||||
shard: impl AsRef<Path>,
|
||||
key_discovery: Option<&str>,
|
||||
) -> Result<(File, Option<PathBuf>)> {
|
||||
) -> Result<(File, Vec<Cert>)> {
|
||||
let key_discovery = key_discovery.map(PathBuf::from);
|
||||
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
||||
|
||||
Ok((File::open(shard)?, key_discovery))
|
||||
// Load certs from path
|
||||
let certs = key_discovery
|
||||
.map(discover_certs)
|
||||
.transpose()?
|
||||
.unwrap_or(vec![]);
|
||||
|
||||
Ok((File::open(shard)?, certs))
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let mut args = env::args();
|
||||
let program_name = args.next().expect("program name");
|
||||
let args = args.collect::<Vec<_>>();
|
||||
let (messages_file, key_discovery) = match args.as_slice() {
|
||||
let (messages_file, cert_list) = match args.as_slice() {
|
||||
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
|
||||
[shard] => validate(shard, None)?,
|
||||
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||
};
|
||||
|
||||
let openpgp = OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt_handler = default_terminal()?;
|
||||
let mut encrypted_messages = parse_messages(messages_file)?;
|
||||
|
||||
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file, prompt_handler)?;
|
||||
print!("{}", smex::encode(bytes));
|
||||
let encrypted_metadata = encrypted_messages
|
||||
.pop_front()
|
||||
.expect("any pgp encrypted message");
|
||||
|
||||
let mut bytes = vec![];
|
||||
|
||||
combine(
|
||||
cert_list,
|
||||
&encrypted_metadata,
|
||||
encrypted_messages.into(),
|
||||
&mut bytes,
|
||||
)?;
|
||||
|
||||
print!("{}", smex::encode(&bytes));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! Decrypt a single OpenPGP shard and encapsulate it for remote transport.
|
||||
//!
|
||||
|
||||
use std::{
|
||||
env,
|
||||
|
@ -7,35 +7,47 @@ use std::{
|
|||
process::ExitCode,
|
||||
};
|
||||
|
||||
use keyfork_prompt::{DefaultTerminal, default_terminal};
|
||||
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||
use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages};
|
||||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
||||
fn validate(
|
||||
shard: impl AsRef<Path>,
|
||||
key_discovery: Option<&str>,
|
||||
) -> Result<(File, Option<PathBuf>)> {
|
||||
let key_discovery = key_discovery.map(PathBuf::from);
|
||||
fn validate<'a>(
|
||||
messages_file: impl AsRef<Path>,
|
||||
key_discovery: impl Into<Option<&'a str>>,
|
||||
) -> Result<(File, Vec<Cert>)> {
|
||||
let key_discovery = key_discovery.into().map(PathBuf::from);
|
||||
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
||||
|
||||
Ok((File::open(shard)?, key_discovery))
|
||||
// Load certs from path
|
||||
let certs = key_discovery
|
||||
.map(discover_certs)
|
||||
.transpose()?
|
||||
.unwrap_or(vec![]);
|
||||
|
||||
Ok((File::open(messages_file)?, certs))
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let mut args = env::args();
|
||||
let program_name = args.next().expect("program name");
|
||||
let args = args.collect::<Vec<_>>();
|
||||
let (messages_file, key_discovery) = match args.as_slice() {
|
||||
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
|
||||
[shard] => validate(shard, None)?,
|
||||
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||
let (messages_file, cert_list) = match args.as_slice() {
|
||||
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
|
||||
[messages_file] => validate(messages_file, None)?,
|
||||
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
|
||||
};
|
||||
|
||||
let openpgp = OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt_handler = default_terminal()?;
|
||||
let mut encrypted_messages = parse_messages(messages_file)?;
|
||||
|
||||
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file, prompt_handler)?;
|
||||
let encrypted_metadata = encrypted_messages
|
||||
.pop_front()
|
||||
.expect("any pgp encrypted message");
|
||||
|
||||
decrypt(
|
||||
&cert_list,
|
||||
&encrypted_metadata,
|
||||
encrypted_messages.make_contiguous(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! Combine OpenPGP shards using remote transport and output the hex-encoded secret.
|
||||
//!
|
||||
|
||||
use std::{
|
||||
env,
|
||||
|
@ -20,7 +20,7 @@ fn run() -> Result<()> {
|
|||
|
||||
let mut bytes = vec![];
|
||||
remote_decrypt(&mut bytes)?;
|
||||
print!("{}", smex::encode(bytes));
|
||||
print!("{}", smex::encode(&bytes));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
//! Split a hex-encoded secret into OpenPGP shards
|
||||
//!
|
||||
|
||||
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
||||
|
||||
use keyfork_prompt::terminal::DefaultTerminal;
|
||||
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Error {
|
||||
Usage(String),
|
||||
Input,
|
||||
Threshold(u8, u8),
|
||||
InvalidCertCount(usize, u8),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
|
@ -18,6 +19,15 @@ impl std::fmt::Display for Error {
|
|||
write!(f, "Usage: {program_name} threshold max key_discovery")
|
||||
}
|
||||
Error::Input => f.write_str("Expected hex encoded input"),
|
||||
Error::Threshold(threshold, max) => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid threshold: 0 < threshold {threshold} <= max {max} < 256"
|
||||
)
|
||||
}
|
||||
Error::InvalidCertCount(count, max) => {
|
||||
write!(f, "Invalid cert count: count {count} != max {max}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,20 +36,31 @@ impl std::error::Error for Error {}
|
|||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
||||
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, u8, PathBuf)> {
|
||||
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, Vec<Cert>)> {
|
||||
let threshold = u8::from_str(threshold)?;
|
||||
let max = u8::from_str(max)?;
|
||||
let key_discovery = PathBuf::from(key_discovery);
|
||||
if threshold > max {
|
||||
return Err(Error::Threshold(threshold, max).into());
|
||||
}
|
||||
|
||||
// Verify path exists
|
||||
std::fs::metadata(&key_discovery)?;
|
||||
|
||||
Ok((threshold, max, key_discovery))
|
||||
// Load certs from path
|
||||
let certs = discover_certs(key_discovery)?;
|
||||
if certs.len() != max.into() {
|
||||
return Err(Error::InvalidCertCount(certs.len(), max).into());
|
||||
}
|
||||
|
||||
Ok((threshold, certs))
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let mut args = env::args();
|
||||
let program_name = args.next().expect("program name");
|
||||
let args = args.collect::<Vec<_>>();
|
||||
let (threshold, max, key_discovery) = match args.as_slice() {
|
||||
let (threshold, cert_list) = match args.as_slice() {
|
||||
[threshold, max, key_discovery] => validate(threshold, max, key_discovery)?,
|
||||
_ => return Err(Error::Usage(program_name).into()),
|
||||
};
|
||||
|
@ -48,12 +69,11 @@ fn run() -> Result<()> {
|
|||
let Some(line) = stdin().lines().next() else {
|
||||
return Err(Error::Input.into());
|
||||
};
|
||||
smex::decode(line?)?
|
||||
smex::decode(&line?)?
|
||||
};
|
||||
|
||||
let openpgp = OpenPGP::<DefaultTerminal>::new();
|
||||
split(threshold, cert_list, &input, std::io::stdout())?;
|
||||
|
||||
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,390 +1,24 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![allow(clippy::expect_fun_call)]
|
||||
|
||||
use std::{
|
||||
io::{stdin, stdout, Read, Write},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::io::{stdin, stdout, Write};
|
||||
|
||||
use aes_gcm::{
|
||||
aead::{consts::U12, Aead},
|
||||
Aes256Gcm, KeyInit, Nonce,
|
||||
aead::{Aead, AeadCore, OsRng},
|
||||
Aes256Gcm, KeyInit,
|
||||
};
|
||||
use base64::prelude::{Engine, BASE64_STANDARD};
|
||||
use hkdf::Hkdf;
|
||||
use keyfork_bug::{bug, POISONED_MUTEX};
|
||||
use keyfork_mnemonic::{English, Mnemonic};
|
||||
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
|
||||
use keyfork_prompt::{
|
||||
validators::{
|
||||
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
|
||||
Validator,
|
||||
},
|
||||
validators::{mnemonic::MnemonicSetValidator, Validator},
|
||||
Message as PromptMessage, PromptHandler, Terminal,
|
||||
};
|
||||
use sha2::Sha256;
|
||||
use sharks::{Share, Sharks};
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
const PLAINTEXT_LENGTH: u8 = 32 // shard
|
||||
+ 1 // index
|
||||
+ 1 // threshold
|
||||
+ 1 // version
|
||||
+ 1; // length;
|
||||
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
|
||||
|
||||
#[cfg(feature = "openpgp")]
|
||||
pub mod openpgp;
|
||||
|
||||
/// A trait to specify where keys can be discovered from, such as a Rust-native type or a path on
|
||||
/// the filesystem that keys may be read from.
|
||||
pub trait KeyDiscovery<F: Format + ?Sized> {
|
||||
/// Discover public keys for the associated format.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if public keys could not be loaded from the given discovery
|
||||
/// mechanism. A discovery mechanism _must_ be able to detect public keys.
|
||||
fn discover_public_keys(&self) -> Result<Vec<F::PublicKey>, F::Error>;
|
||||
|
||||
/// Discover private keys for the associated format.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if private keys could not be loaded from the given
|
||||
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
|
||||
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
|
||||
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
|
||||
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
|
||||
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
|
||||
}
|
||||
|
||||
/// A format to use for splitting and combining secrets.
|
||||
pub trait Format {
|
||||
/// The error type returned from any failed operations.
|
||||
type Error: std::error::Error + 'static;
|
||||
|
||||
/// A type encapsulating a single public key recipient.
|
||||
type PublicKey;
|
||||
|
||||
/// A type encapsulating the private key recipients of shards.
|
||||
type PrivateKeyData;
|
||||
|
||||
/// A type representing a Signer derived from the secret.
|
||||
type SigningKey;
|
||||
|
||||
/// A type representing the parsed, but encrypted, Shard data.
|
||||
type EncryptedData;
|
||||
|
||||
/// Derive a signer
|
||||
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
|
||||
|
||||
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
|
||||
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
|
||||
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
|
||||
/// will use the same key_data for both, ensuring an iteration of this method will match with
|
||||
/// iterations in methods called later.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if encryption to any of the public keys fails.
|
||||
fn format_encrypted_header(
|
||||
&self,
|
||||
signing_key: &Self::SigningKey,
|
||||
key_data: &[Self::PublicKey],
|
||||
threshold: u8,
|
||||
) -> Result<Self::EncryptedData, Self::Error>;
|
||||
|
||||
/// Format a shard encrypted to the given public key, signing with the private key.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the public key used to encrypt the shard is unsuitable
|
||||
/// for encryption, or if an error occurs while encrypting.
|
||||
fn encrypt_shard(
|
||||
&self,
|
||||
shard: &[u8],
|
||||
public_key: &Self::PublicKey,
|
||||
signing_key: &mut Self::SigningKey,
|
||||
) -> Result<Self::EncryptedData, Self::Error>;
|
||||
|
||||
/// Parse the Shard file into a processable type.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the Shard file could not be read from or if the Shard
|
||||
/// file could not be properly parsed.
|
||||
fn parse_shard_file(
|
||||
&self,
|
||||
shard_file: impl Read + Send + Sync,
|
||||
) -> Result<Vec<Self::EncryptedData>, Self::Error>;
|
||||
|
||||
/// Write the Shard data to a Shard file.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the Shard data could not be properly serialized or if the
|
||||
/// Shard file could not be written to.
|
||||
fn format_shard_file(
|
||||
&self,
|
||||
encrypted_data: &[Self::EncryptedData],
|
||||
shard_file: impl Write + Send + Sync,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Decrypt shares and associated metadata from a readable input. For the current version of
|
||||
/// Keyfork, the only associated metadata is a u8 representing the threshold to combine
|
||||
/// secrets.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the shardfile couldn't be read from, if all shards
|
||||
/// could not be decrypted, or if a shard could not be parsed from the decrypted data.
|
||||
fn decrypt_all_shards(
|
||||
&self,
|
||||
private_keys: Option<Self::PrivateKeyData>,
|
||||
encrypted_messages: &[Self::EncryptedData],
|
||||
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||
) -> Result<(Vec<Share>, u8), Self::Error>;
|
||||
|
||||
/// Decrypt a single share and associated metadata from a reaable input. For the current
|
||||
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
|
||||
/// combine secrets.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the shardfile couldn't be read from, if a shard could not
|
||||
/// be decrypted, or if a shard could not be parsed from the decrypted data.
|
||||
fn decrypt_one_shard(
|
||||
&self,
|
||||
private_keys: Option<Self::PrivateKeyData>,
|
||||
encrypted_data: &[Self::EncryptedData],
|
||||
prompt: Arc<Mutex<impl PromptHandler>>,
|
||||
) -> Result<(Share, u8), Self::Error>;
|
||||
|
||||
/// Decrypt multiple shares and combine them to recreate a secret.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the shares can't be decrypted or if the shares can't
|
||||
/// be combined into a secret.
|
||||
fn decrypt_all_shards_to_secret(
|
||||
&self,
|
||||
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||
reader: impl Read + Send + Sync,
|
||||
prompt: impl PromptHandler,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let private_keys = private_key_discovery
|
||||
.map(|p| p.discover_private_keys())
|
||||
.transpose()?;
|
||||
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||
let (shares, threshold) = self.decrypt_all_shards(
|
||||
private_keys,
|
||||
&encrypted_messages,
|
||||
Arc::new(Mutex::new(prompt)),
|
||||
)?;
|
||||
|
||||
let secret = Sharks(threshold)
|
||||
.recover(&shares)
|
||||
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
|
||||
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
/// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the
|
||||
/// shard to the AES key.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if a share can't be decrypted. The method will not return an
|
||||
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
|
||||
/// QR code; instead, a mnemonic prompt will be used.
|
||||
fn decrypt_one_shard_for_transport(
|
||||
&self,
|
||||
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||
reader: impl Read + Send + Sync,
|
||||
prompt: impl PromptHandler,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let prompt = Arc::new(Mutex::new(prompt));
|
||||
|
||||
// parse input
|
||||
let private_keys = private_key_discovery
|
||||
.map(|p| p.discover_private_keys())
|
||||
.transpose()?;
|
||||
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||
|
||||
// establish AES-256-GCM key via ECDH
|
||||
let mut pubkey_data: Option<[u8; 32]> = None;
|
||||
|
||||
// receive remote data via scanning QR code from camera
|
||||
#[cfg(feature = "qrcode")]
|
||||
{
|
||||
prompt
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||
if let Ok(Some(qrcode_content)) =
|
||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
||||
{
|
||||
let decoded_data = BASE64_STANDARD
|
||||
.decode(qrcode_content)
|
||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
|
||||
} else {
|
||||
prompt
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||
};
|
||||
}
|
||||
|
||||
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
||||
let their_pubkey = match pubkey_data {
|
||||
Some(pubkey) => pubkey,
|
||||
None => {
|
||||
let validator = MnemonicValidator {
|
||||
word_length: Some(WordLength::Count(24)),
|
||||
};
|
||||
prompt
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_validated_wordlist::<English, _>(
|
||||
QRCODE_COULDNT_READ,
|
||||
3,
|
||||
validator.to_fn(),
|
||||
)?
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.map_err(|_| InvalidData)?
|
||||
}
|
||||
};
|
||||
|
||||
// create our shared key
|
||||
let our_key = EphemeralSecret::random();
|
||||
let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
|
||||
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
|
||||
assert!(
|
||||
shared_secret.was_contributory(),
|
||||
bug!("shared secret might be insecure")
|
||||
);
|
||||
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
|
||||
|
||||
let mut shared_key_data = [0u8; 256 / 8];
|
||||
hkdf.expand(b"key", &mut shared_key_data)?;
|
||||
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
|
||||
|
||||
let mut nonce_data = [0u8; 12];
|
||||
hkdf.expand(b"nonce", &mut nonce_data)?;
|
||||
let nonce = Nonce::<U12>::from_slice(&nonce_data);
|
||||
|
||||
// decrypt a single shard and create the payload
|
||||
let (share, threshold) =
|
||||
self.decrypt_one_shard(private_keys, &encrypted_messages, prompt.clone())?;
|
||||
let mut payload = Vec::from(&share);
|
||||
payload.insert(0, HUNK_VERSION);
|
||||
payload.insert(1, threshold);
|
||||
assert!(
|
||||
payload.len() < PLAINTEXT_LENGTH as usize,
|
||||
"invalid share length (too long, must be less than {PLAINTEXT_LENGTH} bytes)"
|
||||
);
|
||||
|
||||
// convert plaintext to static-size payload
|
||||
#[allow(clippy::assertions_on_constants)]
|
||||
{
|
||||
assert!(PLAINTEXT_LENGTH < u8::MAX, "length byte can be u8");
|
||||
}
|
||||
|
||||
// NOTE: Previous versions of Keyfork Shard would modify the padding bytes to avoid
|
||||
// duplicate mnemonic words. This version does not include that, and instead uses a
|
||||
// repeated length byte.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let mut plaintext_bytes = [u8::try_from(payload.len()).expect(bug!(
|
||||
"previously asserted length must be < {PLAINTEXT_LENGTH}",
|
||||
PLAINTEXT_LENGTH = PLAINTEXT_LENGTH
|
||||
)); PLAINTEXT_LENGTH as usize];
|
||||
plaintext_bytes[..payload.len()].clone_from_slice(&payload);
|
||||
|
||||
// encrypt data
|
||||
let encrypted_bytes = shared_key.encrypt(nonce, plaintext_bytes.as_slice())?;
|
||||
assert_eq!(
|
||||
encrypted_bytes.len(),
|
||||
ENCRYPTED_LENGTH as usize,
|
||||
bug!("encrypted bytes size != expected len"),
|
||||
);
|
||||
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
|
||||
mnemonic_bytes.copy_from_slice(&encrypted_bytes);
|
||||
|
||||
let payload_mnemonic = Mnemonic::from_array(mnemonic_bytes);
|
||||
|
||||
#[cfg(feature = "qrcode")]
|
||||
{
|
||||
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
||||
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
|
||||
qrcode_data.extend(payload_mnemonic.as_bytes());
|
||||
if let Ok(qrcode) = qrencode(
|
||||
&BASE64_STANDARD.encode(qrcode_data),
|
||||
ErrorCorrection::Highest,
|
||||
) {
|
||||
prompt
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(PromptMessage::Text(
|
||||
concat!(
|
||||
"A QR code will be displayed after this prompt. ",
|
||||
"Send the QR code back to the operator combining the shards. ",
|
||||
"Nobody else should scan this QR code."
|
||||
)
|
||||
.to_string(),
|
||||
))?;
|
||||
prompt
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(PromptMessage::Data(qrcode))?;
|
||||
}
|
||||
}
|
||||
|
||||
prompt
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(PromptMessage::Text(format!(
|
||||
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
|
||||
)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Split a secret into a shard for every shard in keys, with the given Shamir's Secret Sharing
|
||||
/// threshold.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the shares can't be encrypted.
|
||||
fn shard_and_encrypt(
|
||||
&self,
|
||||
threshold: u8,
|
||||
max: u8,
|
||||
secret: &[u8],
|
||||
public_key_discovery: impl KeyDiscovery<Self>,
|
||||
writer: impl Write + Send + Sync,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut signing_key = self.derive_signing_key(secret);
|
||||
|
||||
let sharks = Sharks(threshold);
|
||||
let dealer = sharks.dealer(secret);
|
||||
|
||||
let public_keys = public_key_discovery.discover_public_keys()?;
|
||||
assert!(
|
||||
public_keys.len() < u8::MAX as usize,
|
||||
"must have less than u8::MAX public keys"
|
||||
);
|
||||
assert_eq!(
|
||||
max,
|
||||
public_keys.len() as u8,
|
||||
"max must be equal to amount of public keys"
|
||||
);
|
||||
let max = public_keys.len() as u8;
|
||||
assert!(max >= threshold, "threshold must not exceed max keys");
|
||||
|
||||
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
|
||||
let mut messages = vec![header];
|
||||
for (pk, share) in public_keys.iter().zip(dealer) {
|
||||
let shard = Vec::from(&share);
|
||||
messages.push(self.encrypt_shard(&shard, pk, &mut signing_key)?);
|
||||
}
|
||||
|
||||
self.format_shard_file(&messages, writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum SharksError {
|
||||
|
@ -429,6 +63,7 @@ const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry
|
|||
/// incompatible with the currently running version.
|
||||
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut pm = Terminal::new(stdin(), stdout())?;
|
||||
let wordlist = Wordlist::default();
|
||||
|
||||
let mut iter_count = None;
|
||||
let mut shares = vec![];
|
||||
|
@ -438,22 +73,24 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|||
|
||||
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
|
||||
iter += 1;
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
let nonce_mnemonic =
|
||||
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
|
||||
let our_key = EphemeralSecret::random();
|
||||
let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
|
||||
let key_mnemonic =
|
||||
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
|
||||
|
||||
#[cfg(feature = "qrcode")]
|
||||
{
|
||||
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
||||
let qrcode_data = key_mnemonic.to_bytes();
|
||||
if let Ok(qrcode) = qrencode(
|
||||
&BASE64_STANDARD.encode(qrcode_data),
|
||||
ErrorCorrection::Highest,
|
||||
) {
|
||||
let mut qrcode_data = nonce_mnemonic.to_bytes();
|
||||
qrcode_data.extend(key_mnemonic.as_bytes());
|
||||
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
|
||||
pm.prompt_message(PromptMessage::Text(format!(
|
||||
concat!(
|
||||
"QR code #{iter} will be displayed after this prompt. ",
|
||||
"Send the QR code to the next shardholder. ",
|
||||
"Only the next shardholder should scan the QR code."
|
||||
"A QR code will be displayed after this prompt. ",
|
||||
"Send the QR code to only shardholder {iter}. ",
|
||||
"Nobody else should scan this QR code."
|
||||
),
|
||||
iter = iter
|
||||
)))?;
|
||||
|
@ -463,9 +100,11 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|||
|
||||
pm.prompt_message(PromptMessage::Text(format!(
|
||||
concat!(
|
||||
"Upon request, these words should be sent to the shardholder: ",
|
||||
"{key_mnemonic}"
|
||||
"Upon request, these words should be sent to shardholder {iter}: ",
|
||||
"{nonce_mnemonic} {key_mnemonic}"
|
||||
),
|
||||
iter = iter,
|
||||
nonce_mnemonic = nonce_mnemonic,
|
||||
key_mnemonic = key_mnemonic,
|
||||
)))?;
|
||||
|
||||
|
@ -475,18 +114,10 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|||
#[cfg(feature = "qrcode")]
|
||||
{
|
||||
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||
if let Ok(Some(qrcode_content)) =
|
||||
if let Ok(Some(hex)) =
|
||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
|
||||
{
|
||||
let decoded_data = BASE64_STANDARD
|
||||
.decode(qrcode_content)
|
||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||
assert_eq!(
|
||||
decoded_data.len(),
|
||||
// Include length of public key
|
||||
ENCRYPTED_LENGTH as usize + 32,
|
||||
bug!("invalid payload data")
|
||||
);
|
||||
let decoded_data = smex::decode(&hex)?;
|
||||
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
||||
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
||||
} else {
|
||||
|
@ -501,12 +132,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|||
word_lengths: [24, 39],
|
||||
};
|
||||
|
||||
let [pubkey_mnemonic, payload_mnemonic] = pm
|
||||
.prompt_validated_wordlist::<English, _>(
|
||||
QRCODE_COULDNT_READ,
|
||||
3,
|
||||
validator.to_fn(),
|
||||
)?;
|
||||
let [pubkey_mnemonic, payload_mnemonic] = pm.prompt_validated_wordlist(
|
||||
QRCODE_COULDNT_READ,
|
||||
&wordlist,
|
||||
3,
|
||||
validator.to_fn(),
|
||||
)?;
|
||||
let pubkey = pubkey_mnemonic
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
|
@ -516,28 +147,14 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|||
}
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
payload.len(),
|
||||
ENCRYPTED_LENGTH as usize,
|
||||
bug!("invalid payload data")
|
||||
);
|
||||
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
|
||||
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
|
||||
let mut hkdf_output = [0u8; 256 / 8];
|
||||
hkdf.expand(&[], &mut hkdf_output)?;
|
||||
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
|
||||
|
||||
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey));
|
||||
assert!(
|
||||
shared_secret.was_contributory(),
|
||||
bug!("shared secret might be insecure")
|
||||
);
|
||||
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
|
||||
|
||||
let mut shared_key_data = [0u8; 256 / 8];
|
||||
hkdf.expand(b"key", &mut shared_key_data)?;
|
||||
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
|
||||
|
||||
let mut nonce_data = [0u8; 12];
|
||||
hkdf.expand(b"nonce", &mut nonce_data)?;
|
||||
let nonce = Nonce::<U12>::from_slice(&nonce_data);
|
||||
|
||||
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
|
||||
let payload =
|
||||
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
|
||||
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
|
||||
|
||||
match &mut iter_count {
|
||||
|
@ -552,8 +169,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|||
}
|
||||
}
|
||||
|
||||
let payload_len = payload.last().expect(bug!("payload should not be empty"));
|
||||
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
|
||||
shares.push(payload[HUNK_OFFSET..].to_vec());
|
||||
}
|
||||
|
||||
let shares = shares
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,4 @@
|
|||
#![allow(clippy::expect_fun_call)]
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use keyfork_bug::{bug, POISONED_MUTEX};
|
||||
use keyfork_prompt::{Error as PromptError, PromptHandler};
|
||||
use keyfork_prompt::{Error as PromptError, DefaultTerminal, default_terminal, PromptHandler};
|
||||
|
||||
use super::openpgp::{
|
||||
self,
|
||||
|
@ -27,18 +22,18 @@ pub enum Error {
|
|||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
pub struct Keyring<P: PromptHandler> {
|
||||
pub struct Keyring {
|
||||
full_certs: Vec<Cert>,
|
||||
root: Option<Cert>,
|
||||
pm: Arc<Mutex<P>>,
|
||||
pm: DefaultTerminal,
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> Keyring<P> {
|
||||
pub fn new(certs: impl AsRef<[Cert]>, p: Arc<Mutex<P>>) -> Result<Self> {
|
||||
impl Keyring {
|
||||
pub fn new(certs: impl AsRef<[Cert]>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
full_certs: certs.as_ref().to_vec(),
|
||||
root: Default::default(),
|
||||
pm: p,
|
||||
pm: default_terminal()?,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -62,7 +57,7 @@ impl<P: PromptHandler> Keyring<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
|
||||
impl VerificationHelper for &mut Keyring {
|
||||
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
|
||||
Ok(ids
|
||||
.iter()
|
||||
|
@ -84,23 +79,13 @@ impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
|
|||
aead_algo,
|
||||
} => {}
|
||||
MessageLayer::SignatureGroup { results } => {
|
||||
match &results[..] {
|
||||
[Ok(_)] => {
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
// FIXME: anyhow leak: VerificationError impl std::error::Error
|
||||
// return Err(e.context("Invalid signature"));
|
||||
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
|
||||
}
|
||||
}
|
||||
/*
|
||||
for result in results {
|
||||
if let Err(e) = result {
|
||||
// FIXME: anyhow leak: VerificationError impl std::error::Error
|
||||
// return Err(e.context("Invalid signature"));
|
||||
return Err(anyhow::anyhow!("Invalid signature: {e}"));
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +93,7 @@ impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
|
||||
impl DecryptionHelper for &mut Keyring {
|
||||
fn decrypt<D>(
|
||||
&mut self,
|
||||
pkesks: &[PKESK],
|
||||
|
@ -126,10 +111,12 @@ impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
|
|||
pkesk.recipient().is_wildcard()
|
||||
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
|
||||
}) {
|
||||
#[allow(deprecated, clippy::map_flatten)]
|
||||
let name = cert
|
||||
.userids()
|
||||
.next()
|
||||
.and_then(|userid| userid.userid().name2().transpose())
|
||||
.map(|userid| userid.userid().name().transpose())
|
||||
.flatten()
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten();
|
||||
|
@ -152,8 +139,6 @@ impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
|
|||
};
|
||||
let passphrase = self
|
||||
.pm
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_passphrase(&message)
|
||||
.context("Decryption passphrase")?;
|
||||
secret_key
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
#![allow(clippy::expect_fun_call)]
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use keyfork_bug::{bug, POISONED_MUTEX};
|
||||
use keyfork_prompt::{
|
||||
default_terminal,
|
||||
validators::{PinValidator, Validator},
|
||||
Error as PromptError, Message, PromptHandler,
|
||||
DefaultTerminal, Error as PromptError, Message, PromptHandler,
|
||||
};
|
||||
|
||||
use super::openpgp::{
|
||||
|
@ -71,19 +66,19 @@ fn format_name(input: impl AsRef<str>) -> String {
|
|||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub struct SmartcardManager<P: PromptHandler> {
|
||||
pub struct SmartcardManager {
|
||||
current_card: Option<Card<Open>>,
|
||||
root: Option<Cert>,
|
||||
pm: Arc<Mutex<P>>,
|
||||
pm: DefaultTerminal,
|
||||
pin_cache: HashMap<Fingerprint, String>,
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> SmartcardManager<P> {
|
||||
pub fn new(p: Arc<Mutex<P>>) -> Result<Self> {
|
||||
impl SmartcardManager {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
current_card: None,
|
||||
root: None,
|
||||
pm: p,
|
||||
pm: default_terminal()?,
|
||||
pin_cache: Default::default(),
|
||||
})
|
||||
}
|
||||
|
@ -101,13 +96,9 @@ impl<P: PromptHandler> SmartcardManager<P> {
|
|||
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
|
||||
break c;
|
||||
}
|
||||
self.pm
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(Message::Text(
|
||||
"No smart card was found. Please plug in a smart card and press enter"
|
||||
.to_string(),
|
||||
))?;
|
||||
self.pm.prompt_message(Message::Text(
|
||||
"No smart card was found. Please plug in a smart card and press enter".to_string(),
|
||||
))?;
|
||||
};
|
||||
let mut card = Card::<Open>::new(card_backend).map_err(Error::OpenSmartCard)?;
|
||||
let transaction = card.transaction().map_err(Error::Transaction)?;
|
||||
|
@ -161,19 +152,16 @@ impl<P: PromptHandler> SmartcardManager<P> {
|
|||
}
|
||||
}
|
||||
|
||||
self.pm
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(Message::Text(
|
||||
"Please plug in a smart card and press enter".to_string(),
|
||||
))?;
|
||||
self.pm.prompt_message(Message::Text(
|
||||
"Please plug in a smart card and press enter".to_string(),
|
||||
))?;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
|
||||
impl VerificationHelper for &mut SmartcardManager {
|
||||
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
|
||||
#[allow(clippy::flat_map_option)]
|
||||
Ok(ids
|
||||
|
@ -193,23 +181,12 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
|
|||
aead_algo,
|
||||
} => {}
|
||||
MessageLayer::SignatureGroup { results } => {
|
||||
match &results[..] {
|
||||
[Ok(_)] => {
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
// FIXME: anyhow leak: VerificationError impl std::error::Error
|
||||
// return Err(e.context("Invalid signature"));
|
||||
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
|
||||
}
|
||||
}
|
||||
/*
|
||||
for result in results {
|
||||
if let Err(e) = result {
|
||||
return Err(anyhow::anyhow!("Invalid signature: {e}"));
|
||||
// FIXME: anyhow leak
|
||||
return Err(anyhow::anyhow!("Verification error: {}", e.to_string()));
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -217,7 +194,7 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
|
||||
impl DecryptionHelper for &mut SmartcardManager {
|
||||
fn decrypt<D>(
|
||||
&mut self,
|
||||
pkesks: &[PKESK],
|
||||
|
@ -277,8 +254,6 @@ impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
|
|||
};
|
||||
let temp_pin = self
|
||||
.pm
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_validated_passphrase(&message, 3, &pin_validator)?;
|
||||
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
|
||||
match verification_status {
|
||||
|
@ -290,8 +265,6 @@ impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
|
|||
// NOTE: This should not be hit, because of the above validator.
|
||||
Err(CardError::CardStatus(StatusBytes::IncorrectParametersCommandDataField)) => {
|
||||
self.pm
|
||||
.lock()
|
||||
.expect(bug!(POISONED_MUTEX))
|
||||
.prompt_message(Message::Text("Invalid PIN length entered.".to_string()))?;
|
||||
}
|
||||
Err(_) => {}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "keyfork"
|
||||
version = "0.2.4"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
|
@ -23,25 +23,23 @@ sequoia-crypto-backend-openssl = ["sequoia-openpgp/crypto-openssl"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
keyfork-bin = { workspace = true }
|
||||
keyforkd = { workspace = true, features = ["tracing"] }
|
||||
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] }
|
||||
keyfork-derive-util = { workspace = true, default-features = true }
|
||||
keyfork-derive-openpgp = { workspace = true }
|
||||
keyfork-derive-path-data = { workspace = true }
|
||||
keyfork-entropy = { workspace = true }
|
||||
keyfork-mnemonic = { workspace = true }
|
||||
keyfork-prompt = { workspace = true }
|
||||
keyfork-qrcode = { workspace = true, default-features = false }
|
||||
keyfork-shard = { workspace = true, default-features = false, features = ["openpgp", "openpgp-card", "qrcode"] }
|
||||
smex = { workspace = true }
|
||||
keyforkd = { version = "0.1.0", path = "../daemon/keyforkd", features = ["tracing"] }
|
||||
keyforkd-client = { version = "0.1.0", path = "../daemon/keyforkd-client", default-features = false, features = ["ed25519"] }
|
||||
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp" }
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../derive/keyfork-derive-util", default-features = false, features = ["ed25519"] }
|
||||
keyfork-entropy = { version = "0.1.0", path = "../util/keyfork-entropy" }
|
||||
keyfork-mnemonic-util = { version = "0.1.0", path = "../util/keyfork-mnemonic-util" }
|
||||
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt" }
|
||||
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode" }
|
||||
keyfork-shard = { version = "0.1.0", path = "../keyfork-shard" }
|
||||
smex = { version = "0.1.0", path = "../util/smex" }
|
||||
|
||||
clap = { version = "4.4.2", features = ["derive", "env", "wrap_help"] }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
card-backend-pcsc = { workspace = true }
|
||||
openpgp-card-sequoia = { workspace = true }
|
||||
openpgp-card = { workspace = true }
|
||||
thiserror = "1.0.48"
|
||||
serde = { version = "1.0.192", features = ["derive"] }
|
||||
tokio = { version = "1.35.1", default-features = false, features = ["rt-multi-thread"] }
|
||||
card-backend-pcsc = "0.5.0"
|
||||
openpgp-card-sequoia = { version = "0.2.0", default-features = false }
|
||||
openpgp-card = "0.4.1"
|
||||
clap_complete = { version = "4.4.6", optional = true }
|
||||
sequoia-openpgp = { workspace = true }
|
||||
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] }
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
# Keyfork: The Kitchen Sink of Entropy
|
||||
|
||||
**Note:** Keyfork operations are meant to be run on an airgapped machine and
|
||||
Keyfork will error if either any network interfaces are detected or if Keyfork
|
||||
is running on a system with a kernel using an insecure random number generator.
|
||||
|
||||
An all-inclusive crate encapsulating end-user functionality of the Keyfork
|
||||
ecosystem, the Keyfork binary includes all mechanisms that should be exposed to
|
||||
the user when running Keyfork. Information about what operations Keyfork
|
||||
performs are available in detail by running `keyfork help` (each subcommand has
|
||||
thorough documentation) or in the [`docs`] mdBook, but here's a quick overview:
|
||||
|
||||
## Getting Started with Keyfork
|
||||
|
||||
Keyfork offers two options for getting started. For multi-user setups, it is
|
||||
best to look at the detailed documentation for Keyfork Shard. For single-user
|
||||
setups, `keyfork mnemonic generate` will (by default) create a 256-bit mnemonic
|
||||
phrase that can be used to start Keyfork. *Store this phrase*, as it's the only
|
||||
way you'll be able to start Keyfork in the future. It is recommended to use a
|
||||
mnemonic recovery sheet or a printed-steel solution such as the [Billfodl] or
|
||||
[Cryptosteel Capsule].
|
||||
|
||||
```sh
|
||||
keyfork mnemonic generate
|
||||
```
|
||||
|
||||
Once a mnemonic has been generated and stored in a secure manner, Keyfork can
|
||||
be started by "recovering" the server from the mnemonic backup mechanism:
|
||||
|
||||
```sh
|
||||
keyfork recover mnemonic
|
||||
```
|
||||
|
||||
## Deriving Keys
|
||||
|
||||
Keyfork's primary goal is to derive keys. These keys can later be used for
|
||||
things such as signing documents and artifacts or decrypting payloads.
|
||||
Keyfork's first derivation target is OpenPGP, a protocol supporting many
|
||||
cryptographic operations. OpenPGP keys require a User ID, which can be used to
|
||||
identify the owner of the key, either by name or by email. To get an OpenPGP
|
||||
public key (more accurately known as a "cert"), the [`sq`][sq] tool is used to
|
||||
convert a key to a certificate:
|
||||
|
||||
```sh
|
||||
keyfork derive openpgp "John Doe <jdoe@example.com>" | sq key extract-cert
|
||||
```
|
||||
|
||||
All Keyfork derivations are intended to be reproducible. Because of this,
|
||||
Keyfork derived keys can be recreated at any time, only requiring the knowledge
|
||||
of how the key was made.
|
||||
|
||||
[`docs`]: /public/keyfork/src/branch/main/docs/src/SUMMARY.md
|
||||
[Billfodl]: https://privacypros.io/products/the-billfodl/
|
||||
[Cryptosteel Capsule]: https://cryptosteel.com/product/cryptosteel-capsule-solo/
|
||||
[sq]: https://gitlab.com/sequoia-pgp/sequoia-sq/
|
|
@ -1,17 +1,16 @@
|
|||
use super::Keyfork;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use keyfork_derive_openpgp::{
|
||||
openpgp::{
|
||||
armor::{Kind, Writer},
|
||||
packet::UserID,
|
||||
serialize::Marshal,
|
||||
types::KeyFlags,
|
||||
},
|
||||
XPrvKey,
|
||||
use keyfork_derive_openpgp::openpgp::{
|
||||
armor::{Kind, Writer},
|
||||
packet::UserID,
|
||||
serialize::Marshal,
|
||||
types::KeyFlags,
|
||||
};
|
||||
use keyfork_derive_util::{
|
||||
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
|
||||
DerivationIndex, DerivationPath,
|
||||
};
|
||||
use keyfork_derive_util::DerivationIndex;
|
||||
use keyfork_derive_path_data::paths;
|
||||
use keyforkd_client::Client;
|
||||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
@ -20,54 +19,49 @@ type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
|||
pub enum DeriveSubcommands {
|
||||
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
|
||||
/// ASCII Armor, a format usable by most programs using OpenPGP.
|
||||
///
|
||||
/// Certificates are created with a default expiration of one day, but may be configured to
|
||||
/// expire later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as
|
||||
/// "15d" (15 days), "1m" (one month), or "2y" (two years).
|
||||
///
|
||||
/// It is recommended to use the default expiration of one day and to change the expiration
|
||||
/// using an external utility, to ensure the Certify key is usable.
|
||||
#[command(name = "openpgp")]
|
||||
OpenPGP(OpenPGP)
|
||||
}
|
||||
|
||||
#[derive(Args, Clone, Debug)]
|
||||
pub struct OpenPGP {
|
||||
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
||||
user_id: String,
|
||||
OpenPGP {
|
||||
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
||||
user_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl DeriveSubcommands {
|
||||
fn handle(&self, account: DerivationIndex) -> Result<()> {
|
||||
match self {
|
||||
DeriveSubcommands::OpenPGP(opgp) => opgp.handle(account),
|
||||
}
|
||||
}
|
||||
}
|
||||
DeriveSubcommands::OpenPGP { user_id } => {
|
||||
let mut pgp_u32 = [0u8; 4];
|
||||
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
|
||||
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||
let path = DerivationPath::default()
|
||||
.chain_push(chain)
|
||||
.chain_push(account);
|
||||
// TODO: should this be customizable?
|
||||
let subkeys = vec![
|
||||
KeyFlags::empty().set_certification(),
|
||||
KeyFlags::empty().set_signing(),
|
||||
KeyFlags::empty()
|
||||
.set_transport_encryption()
|
||||
.set_storage_encryption(),
|
||||
KeyFlags::empty().set_authentication(),
|
||||
];
|
||||
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
|
||||
let derived_data: DerivationResponse = Client::discover_socket()?
|
||||
.request(&request.into())?
|
||||
.try_into()?;
|
||||
let default_userid = UserID::from(user_id.as_str());
|
||||
let cert = keyfork_derive_openpgp::derive(derived_data, &subkeys, &default_userid)?;
|
||||
|
||||
impl OpenPGP {
|
||||
pub fn handle(&self, account: DerivationIndex) -> Result<()> {
|
||||
let path = paths::OPENPGP.clone().chain_push(account);
|
||||
// TODO: should this be customizable?
|
||||
let subkeys = vec![
|
||||
KeyFlags::empty().set_certification(),
|
||||
KeyFlags::empty().set_signing(),
|
||||
KeyFlags::empty()
|
||||
.set_transport_encryption()
|
||||
.set_storage_encryption(),
|
||||
KeyFlags::empty().set_authentication(),
|
||||
];
|
||||
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
|
||||
let default_userid = UserID::from(self.user_id.as_str());
|
||||
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
|
||||
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
|
||||
|
||||
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
|
||||
for packet in cert.into_packets() {
|
||||
packet.serialize(&mut w)?;
|
||||
}
|
||||
|
||||
for packet in cert.into_packets2() {
|
||||
packet.serialize(&mut w)?;
|
||||
w.finalize()?;
|
||||
}
|
||||
}
|
||||
|
||||
w.finalize()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -78,10 +72,6 @@ pub struct Derive {
|
|||
command: DeriveSubcommands,
|
||||
|
||||
/// Account ID. Required for all derivations.
|
||||
///
|
||||
/// An account ID may not be relevant for the derivation being performed, but the lack of an
|
||||
/// account ID can often come as a hindrance in the future. As such, it is always required. If
|
||||
/// the account ID is not relevant, it is assumed to be `0`.
|
||||
#[arg(long, global = true, default_value = "0")]
|
||||
account_id: u32,
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ impl MnemonicSeedSource {
|
|||
MnemonicSeedSource::Tarot => todo!(),
|
||||
MnemonicSeedSource::Dice => todo!(),
|
||||
};
|
||||
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?;
|
||||
let mnemonic = keyfork_mnemonic_util::Mnemonic::from_entropy(&seed, Default::default())?;
|
||||
Ok(mnemonic.to_string())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ mod wizard;
|
|||
|
||||
/// The Kitchen Sink of Entropy.
|
||||
#[derive(Parser, Clone, Debug)]
|
||||
#[command(author, version, about, long_about)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Keyfork {
|
||||
// Global options
|
||||
#[command(subcommand)]
|
||||
|
@ -20,51 +20,25 @@ pub struct Keyfork {
|
|||
pub enum KeyforkCommands {
|
||||
/// Derive keys of various formats. These commands require that the Keyfork server is running,
|
||||
/// which can be started by running a `keyfork recover` command.
|
||||
///
|
||||
/// Derived keys are reproducible: assuming the same arguments are used when deriving a key for
|
||||
/// a second time, the key will be _functionally_ equivalent. This means keys don't need to be
|
||||
/// persisted to cold storage or left hot in a running program. They can be derived when
|
||||
/// they're needed and forgotten when they're not.
|
||||
Derive(derive::Derive),
|
||||
|
||||
/// Mnemonic generation and persistence utilities.
|
||||
Mnemonic(mnemonic::Mnemonic),
|
||||
|
||||
/// Splitting and combining secrets, using Shamir's Secret Sharing.
|
||||
///
|
||||
/// Keys can be split such that a certain amount of users, from a potentially even-larger
|
||||
/// amount of users, can be used to recreate a key. This creates resilience for a key, as in a
|
||||
/// "seven of nine" scenario, nine people in total are capable of recreating a key, but only
|
||||
/// seven may be required.
|
||||
Shard(shard::Shard),
|
||||
|
||||
/// Derive and deploy keys to hardware.
|
||||
///
|
||||
/// Keys existing in hardware creates a situation where it is unlikely (but not impossible) for
|
||||
/// a key to be extracted. While a key in memory could be captured by a rootkit or some other
|
||||
/// privilege escalation mechanism, a key in hardware would require a hardware exploit to
|
||||
/// extract the key.
|
||||
///
|
||||
/// It is recommended to provision keys whenever possible, as opposed to deriving them.
|
||||
#[command(subcommand_negates_reqs(true))]
|
||||
Provision(provision::Provision),
|
||||
|
||||
/// Recover a seed using the requested recovery mechanism and start the Keyfork server.
|
||||
///
|
||||
/// Once the Keyfork server is started, derivation requests can be performed. The Keyfork seed
|
||||
/// is kept solely in the Keyfork server. Derivations with less than two indices are not
|
||||
/// permitted, to ensure a seed often used to derive keys for multiple different paths is not
|
||||
/// leaked by any individual deriver.
|
||||
Recover(recover::Recover),
|
||||
|
||||
/// Utilities to automatically manage the setup of Keyfork.
|
||||
Wizard(wizard::Wizard),
|
||||
|
||||
/// Print an autocompletion file to standard output.
|
||||
///
|
||||
/// Keyfork does not manage the installation of completion files. Consult the documentation for
|
||||
/// the shell for which documentation has been generated on the appropriate location to store
|
||||
/// completion files.
|
||||
#[cfg(feature = "completion")]
|
||||
Completion {
|
||||
#[arg(value_enum)]
|
||||
|
|
|
@ -2,9 +2,11 @@ use super::Keyfork;
|
|||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use keyfork_mnemonic::{English, Mnemonic};
|
||||
use keyfork_prompt::{default_terminal, DefaultTerminal};
|
||||
use keyfork_shard::{remote_decrypt, Format};
|
||||
use keyfork_mnemonic_util::Mnemonic;
|
||||
use keyfork_shard::{
|
||||
openpgp::{combine, discover_certs, parse_messages},
|
||||
remote_decrypt,
|
||||
};
|
||||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
||||
|
@ -35,14 +37,15 @@ impl RecoverSubcommands {
|
|||
} => {
|
||||
let content = std::fs::read_to_string(shard_file)?;
|
||||
if content.contains("BEGIN PGP MESSAGE") {
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt_handler = default_terminal()?;
|
||||
// TODO: remove .clone() by making handle() consume self
|
||||
let seed = openpgp.decrypt_all_shards_to_secret(
|
||||
key_discovery.as_deref(),
|
||||
content.as_bytes(),
|
||||
prompt_handler,
|
||||
)?;
|
||||
let certs = key_discovery
|
||||
.as_ref()
|
||||
.map(discover_certs)
|
||||
.transpose()?
|
||||
.unwrap_or(vec![]);
|
||||
let mut messages = parse_messages(content.as_bytes())?;
|
||||
let metadata = messages.pop_front().expect("any pgp encrypted message");
|
||||
let mut seed = vec![];
|
||||
combine(certs, &metadata, messages.into(), &mut seed)?;
|
||||
Ok(seed)
|
||||
} else {
|
||||
panic!("unknown format of shard file");
|
||||
|
@ -55,6 +58,7 @@ impl RecoverSubcommands {
|
|||
}
|
||||
RecoverSubcommands::Mnemonic {} => {
|
||||
use keyfork_prompt::{
|
||||
default_terminal,
|
||||
validators::{
|
||||
mnemonic::{MnemonicChoiceValidator, WordLength},
|
||||
Validator,
|
||||
|
@ -65,8 +69,9 @@ impl RecoverSubcommands {
|
|||
let validator = MnemonicChoiceValidator {
|
||||
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
|
||||
};
|
||||
let mnemonic = term.prompt_validated_wordlist::<English, _>(
|
||||
let mnemonic = term.prompt_validated_wordlist(
|
||||
"Mnemonic: ",
|
||||
&Default::default(),
|
||||
3,
|
||||
validator.to_fn(),
|
||||
)?;
|
||||
|
@ -85,7 +90,7 @@ pub struct Recover {
|
|||
impl Recover {
|
||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||
let seed = self.command.handle()?;
|
||||
let mnemonic = Mnemonic::try_from_slice(&seed)?;
|
||||
let mnemonic = Mnemonic::from_entropy(&seed, Default::default())?;
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use super::Keyfork;
|
||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||
use keyfork_prompt::{default_terminal, DefaultTerminal};
|
||||
use keyfork_shard::Format as _;
|
||||
use std::{
|
||||
io::{stdin, stdout, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
|
@ -33,23 +31,27 @@ trait ShardExec {
|
|||
&self,
|
||||
threshold: u8,
|
||||
max: u8,
|
||||
key_discovery: &Path,
|
||||
key_discovery: impl AsRef<Path>,
|
||||
secret: &[u8],
|
||||
output: &mut (impl Write + Send + Sync),
|
||||
) -> Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
fn combine(
|
||||
&self,
|
||||
key_discovery: Option<&Path>,
|
||||
input: impl Read + Send + Sync,
|
||||
output: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
fn decrypt(
|
||||
fn combine<T>(
|
||||
&self,
|
||||
key_discovery: Option<&Path>,
|
||||
key_discovery: Option<T>,
|
||||
input: impl Read + Send + Sync,
|
||||
) -> Result<(), Box<dyn std::error::Error>>;
|
||||
output: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
where
|
||||
T: AsRef<Path>;
|
||||
|
||||
fn decrypt<T>(
|
||||
&self,
|
||||
key_discovery: Option<T>,
|
||||
input: impl Read + Send + Sync,
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
where
|
||||
T: AsRef<Path>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -60,36 +62,77 @@ impl ShardExec for OpenPGP {
|
|||
&self,
|
||||
threshold: u8,
|
||||
max: u8,
|
||||
key_discovery: &Path,
|
||||
key_discovery: impl AsRef<Path>,
|
||||
secret: &[u8],
|
||||
output: &mut (impl Write + Send + Sync),
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let opgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
|
||||
}
|
||||
|
||||
fn combine(
|
||||
&self,
|
||||
key_discovery: Option<&Path>,
|
||||
input: impl Read + Send + Sync,
|
||||
output: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt = default_terminal()?;
|
||||
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?;
|
||||
write!(output, "{}", smex::encode(bytes))?;
|
||||
// Get certs and input
|
||||
let certs = keyfork_shard::openpgp::discover_certs(key_discovery.as_ref())?;
|
||||
assert_eq!(
|
||||
certs.len(),
|
||||
max.into(),
|
||||
"cert count {} != max {max}",
|
||||
certs.len()
|
||||
);
|
||||
keyfork_shard::openpgp::split(threshold, certs, secret, output).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn combine<T>(
|
||||
&self,
|
||||
key_discovery: Option<T>,
|
||||
input: impl Read + Send + Sync,
|
||||
output: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
where
|
||||
T: AsRef<Path>,
|
||||
{
|
||||
let certs = key_discovery
|
||||
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
|
||||
.transpose()?
|
||||
.unwrap_or(vec![]);
|
||||
|
||||
let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?;
|
||||
let encrypted_metadata = encrypted_messages
|
||||
.pop_front()
|
||||
.expect("any pgp encrypted message");
|
||||
|
||||
let mut bytes = vec![];
|
||||
|
||||
keyfork_shard::openpgp::combine(
|
||||
certs,
|
||||
&encrypted_metadata,
|
||||
encrypted_messages.into(),
|
||||
&mut bytes,
|
||||
)?;
|
||||
|
||||
write!(output, "{}", smex::encode(&bytes))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt(
|
||||
fn decrypt<T>(
|
||||
&self,
|
||||
key_discovery: Option<&Path>,
|
||||
key_discovery: Option<T>,
|
||||
input: impl Read + Send + Sync,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
|
||||
let prompt = default_terminal()?;
|
||||
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
where
|
||||
T: AsRef<Path>,
|
||||
{
|
||||
let certs = key_discovery
|
||||
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
|
||||
.transpose()?
|
||||
.unwrap_or(vec![]);
|
||||
|
||||
let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?;
|
||||
let encrypted_metadata = encrypted_messages
|
||||
.pop_front()
|
||||
.expect("any pgp encrypted message");
|
||||
|
||||
keyfork_shard::openpgp::decrypt(
|
||||
&certs,
|
||||
&encrypted_metadata,
|
||||
encrypted_messages.make_contiguous(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -182,7 +225,7 @@ impl ShardSubcommands {
|
|||
|
||||
match format {
|
||||
Some(Format::OpenPGP(o)) => {
|
||||
o.decrypt(key_discovery.as_deref(), shard_content.as_bytes())
|
||||
o.decrypt(key_discovery.as_ref(), shard_content.as_bytes())
|
||||
}
|
||||
Some(Format::P256(_p)) => todo!(),
|
||||
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
||||
|
@ -199,7 +242,7 @@ impl ShardSubcommands {
|
|||
|
||||
match format {
|
||||
Some(Format::OpenPGP(o)) => o.combine(
|
||||
key_discovery.as_deref(),
|
||||
key_discovery.as_ref(),
|
||||
shard_content.as_bytes(),
|
||||
&mut stdout,
|
||||
),
|
||||
|
|
|
@ -1,41 +1,27 @@
|
|||
use super::Keyfork;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
|
||||
|
||||
use card_backend_pcsc::PcscBackend;
|
||||
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
|
||||
|
||||
use keyfork_derive_openpgp::{
|
||||
openpgp::{
|
||||
self,
|
||||
armor::{Kind, Writer},
|
||||
packet::UserID,
|
||||
serialize::Marshal,
|
||||
types::KeyFlags,
|
||||
Cert,
|
||||
},
|
||||
XPrv,
|
||||
use keyfork_derive_openpgp::openpgp::{self, packet::UserID, types::KeyFlags, Cert};
|
||||
use keyfork_derive_util::{
|
||||
request::{DerivationAlgorithm, DerivationRequest},
|
||||
DerivationIndex, DerivationPath,
|
||||
};
|
||||
use keyfork_derive_path_data::paths;
|
||||
use keyfork_derive_util::DerivationIndex;
|
||||
use keyfork_mnemonic::Mnemonic;
|
||||
use keyfork_prompt::{
|
||||
default_terminal,
|
||||
validators::{SecurePinValidator, Validator},
|
||||
DefaultTerminal, Message, PromptHandler,
|
||||
validators::{PinValidator, Validator},
|
||||
Message, PromptHandler, Terminal,
|
||||
};
|
||||
|
||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Invalid PIN length: {0}")]
|
||||
pub struct PinLength(usize);
|
||||
|
||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||
|
||||
// TODO: refactor to use mnemonic derived seed instead of 256 bit entropy to allow for possible
|
||||
// recovery in the future.
|
||||
fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
|
||||
fn derive_key(seed: &[u8], index: u8) -> Result<Cert> {
|
||||
let subkeys = vec![
|
||||
KeyFlags::empty().set_certification(),
|
||||
KeyFlags::empty().set_signing(),
|
||||
|
@ -45,13 +31,21 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
|
|||
KeyFlags::empty().set_authentication(),
|
||||
];
|
||||
|
||||
let mut pgp_u32 = [0u8; 4];
|
||||
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
|
||||
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||
let mut shrd_u32 = [0u8; 4];
|
||||
shrd_u32[..].copy_from_slice(&"shrd".bytes().collect::<Vec<u8>>());
|
||||
let account = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||
let subkey = DerivationIndex::new(u32::from(index), true)?;
|
||||
let path = paths::OPENPGP_SHARD.clone().chain_push(subkey);
|
||||
let xprv = XPrv::new(seed)
|
||||
.expect("could not construct master key from seed")
|
||||
.derive_path(&path)?;
|
||||
let path = DerivationPath::default()
|
||||
.chain_push(chain)
|
||||
.chain_push(account)
|
||||
.chain_push(subkey);
|
||||
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
|
||||
let response = request.derive_with_master_seed(seed.to_vec())?;
|
||||
let userid = UserID::from(format!("Keyfork Shard {index}"));
|
||||
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
|
||||
let cert = keyfork_derive_openpgp::derive(response, &subkeys, &userid)?;
|
||||
Ok(cert)
|
||||
}
|
||||
|
||||
|
@ -103,200 +97,121 @@ fn factory_reset_current_card(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_shard_secret(
|
||||
threshold: u8,
|
||||
max: u8,
|
||||
keys_per_shard: u8,
|
||||
output_file: &Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
let seed = keyfork_entropy::generate_entropy_of_size(256 / 8)?;
|
||||
let mut pm = Terminal::new(std::io::stdin(), std::io::stderr())?;
|
||||
let mut certs = vec![];
|
||||
let mut seen_cards: HashSet<String> = HashSet::new();
|
||||
let stdout = std::io::stdout();
|
||||
if output_file.is_none() {
|
||||
assert!(
|
||||
!stdout.is_terminal(),
|
||||
"not printing shard to terminal, redirect output"
|
||||
);
|
||||
}
|
||||
|
||||
let user_pin_validator = PinValidator {
|
||||
min_length: Some(6),
|
||||
..Default::default()
|
||||
}
|
||||
.to_fn();
|
||||
let admin_pin_validator = PinValidator {
|
||||
min_length: Some(8),
|
||||
..Default::default()
|
||||
}
|
||||
.to_fn();
|
||||
|
||||
for index in 0..max {
|
||||
let cert = derive_key(&seed, index)?;
|
||||
for i in 0..keys_per_shard {
|
||||
pm.prompt_message(Message::Text(format!(
|
||||
"Please remove all keys and insert key #{} for user #{}",
|
||||
i + 1,
|
||||
index + 1,
|
||||
)))?;
|
||||
let card_backend = loop {
|
||||
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
|
||||
break c;
|
||||
}
|
||||
pm.prompt_message(Message::Text(
|
||||
"No smart card was found. Please plug in a smart card and press enter"
|
||||
.to_string(),
|
||||
))?;
|
||||
};
|
||||
let user_pin = pm.prompt_validated_passphrase(
|
||||
"Please enter the new smartcard User PIN: ",
|
||||
3,
|
||||
&user_pin_validator,
|
||||
)?;
|
||||
let admin_pin = pm.prompt_validated_passphrase(
|
||||
"Please enter the new smartcard Admin PIN: ",
|
||||
3,
|
||||
&admin_pin_validator,
|
||||
)?;
|
||||
factory_reset_current_card(
|
||||
&mut seen_cards,
|
||||
user_pin.trim(),
|
||||
admin_pin.trim(),
|
||||
&cert,
|
||||
card_backend,
|
||||
)?;
|
||||
}
|
||||
certs.push(cert);
|
||||
}
|
||||
|
||||
if let Some(output_file) = output_file {
|
||||
let output = File::create(output_file)?;
|
||||
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
|
||||
} else {
|
||||
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Clone, Debug)]
|
||||
pub enum WizardSubcommands {
|
||||
GenerateShardSecret(GenerateShardSecret),
|
||||
BottomsUp(BottomsUp),
|
||||
}
|
||||
/// Create a 256 bit secret and shard the secret to smart cards.
|
||||
///
|
||||
/// Smart cards will need to be plugged in periodically during the wizard, where they will be factory reset and
|
||||
/// provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered with `keyfork recover shard` or
|
||||
/// `keyfork recover remote-shard`. The share file will be printed to standard output.
|
||||
GenerateShardSecret {
|
||||
/// The minimum amount of keys required to decrypt the secret.
|
||||
#[arg(long)]
|
||||
threshold: u8,
|
||||
|
||||
/// Create a 256 bit secret and shard the secret to smart cards.
|
||||
///
|
||||
/// Smart cards will need to be plugged in periodically during the wizard, where they will be
|
||||
/// factory reset and provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered
|
||||
/// with `keyfork recover shard` or `keyfork recover remote-shard`. The share file will be printed
|
||||
/// to standard output.
|
||||
#[derive(Args, Clone, Debug)]
|
||||
pub struct GenerateShardSecret {
|
||||
/// The minimum amount of keys required to decrypt the secret.
|
||||
#[arg(long)]
|
||||
threshold: u8,
|
||||
/// The maximum amount of shards.
|
||||
#[arg(long)]
|
||||
max: u8,
|
||||
|
||||
/// The maximum amount of shards.
|
||||
#[arg(long)]
|
||||
max: u8,
|
||||
/// The amount of smart cards to provision per-shard.
|
||||
#[arg(long, default_value = "1")]
|
||||
keys_per_shard: u8,
|
||||
|
||||
/// The amount of smart cards to provision per-shard.
|
||||
#[arg(long, default_value = "1")]
|
||||
keys_per_shard: u8,
|
||||
|
||||
/// The file to write the generated shard file to.
|
||||
#[arg(long)]
|
||||
output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Create a 256 bit secret and shard the secret to previously known OpenPGP certificates,
|
||||
/// deriving the default OpenPGP certificate for the secret.
|
||||
///
|
||||
/// This command was purpose-built for DEFCON and is not intended to be used normally, as it
|
||||
/// implies keys used for sharding have been generated by a custom source.
|
||||
#[derive(Args, Clone, Debug)]
|
||||
pub struct BottomsUp {
|
||||
/// The location of OpenPGP certificates to use when sharding.
|
||||
key_discovery: PathBuf,
|
||||
|
||||
/// The minimum amount of keys required to decrypt the secret.
|
||||
#[arg(long)]
|
||||
threshold: u8,
|
||||
|
||||
/// The file to write the generated shard file to.
|
||||
#[arg(long)]
|
||||
output_shardfile: PathBuf,
|
||||
|
||||
/// The file to write the generated OpenPGP certificate to.
|
||||
#[arg(long)]
|
||||
output_cert: PathBuf,
|
||||
|
||||
/// The User ID for the generated OpenPGP certificate.
|
||||
#[arg(long, default_value = "Disaster Recovery")]
|
||||
user_id: String,
|
||||
/// The file to write the generated shard file to.
|
||||
#[arg(long)]
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl WizardSubcommands {
|
||||
// dispatch
|
||||
fn handle(&self) -> Result<()> {
|
||||
match self {
|
||||
WizardSubcommands::GenerateShardSecret(gss) => gss.handle(),
|
||||
WizardSubcommands::BottomsUp(bu) => bu.handle(),
|
||||
WizardSubcommands::GenerateShardSecret {
|
||||
threshold,
|
||||
max,
|
||||
keys_per_shard,
|
||||
output,
|
||||
} => generate_shard_secret(*threshold, *max, *keys_per_shard, output),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateShardSecret {
|
||||
fn handle(&self) -> Result<()> {
|
||||
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
|
||||
let mut pm = default_terminal()?;
|
||||
let mut certs = vec![];
|
||||
let mut seen_cards: HashSet<String> = HashSet::new();
|
||||
let stdout = std::io::stdout();
|
||||
if self.output.is_none() {
|
||||
assert!(
|
||||
!stdout.is_terminal(),
|
||||
"not printing shard to terminal, redirect output"
|
||||
);
|
||||
}
|
||||
|
||||
let user_pin_validator = SecurePinValidator {
|
||||
min_length: Some(6),
|
||||
..Default::default()
|
||||
}
|
||||
.to_fn();
|
||||
let admin_pin_validator = SecurePinValidator {
|
||||
min_length: Some(8),
|
||||
..Default::default()
|
||||
}
|
||||
.to_fn();
|
||||
|
||||
for index in 0..self.max {
|
||||
let cert = derive_key(seed, index)?;
|
||||
for i in 0..self.keys_per_shard {
|
||||
pm.prompt_message(Message::Text(format!(
|
||||
"Please remove all keys and insert key #{} for user #{}",
|
||||
(i as u16) + 1,
|
||||
(index as u16) + 1,
|
||||
)))?;
|
||||
let card_backend = loop {
|
||||
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
|
||||
break c;
|
||||
}
|
||||
pm.prompt_message(Message::Text(
|
||||
"No smart card was found. Please plug in a smart card and press enter"
|
||||
.to_string(),
|
||||
))?;
|
||||
};
|
||||
let user_pin = pm.prompt_validated_passphrase(
|
||||
"Please enter the new smartcard User PIN: ",
|
||||
3,
|
||||
&user_pin_validator,
|
||||
)?;
|
||||
let admin_pin = pm.prompt_validated_passphrase(
|
||||
"Please enter the new smartcard Admin PIN: ",
|
||||
3,
|
||||
&admin_pin_validator,
|
||||
)?;
|
||||
factory_reset_current_card(
|
||||
&mut seen_cards,
|
||||
user_pin.trim(),
|
||||
admin_pin.trim(),
|
||||
&cert,
|
||||
card_backend,
|
||||
)?;
|
||||
}
|
||||
certs.push(cert);
|
||||
}
|
||||
|
||||
let opgp = OpenPGP::<DefaultTerminal>::new();
|
||||
|
||||
if let Some(output_file) = self.output.as_ref() {
|
||||
let output = File::create(output_file)?;
|
||||
opgp.shard_and_encrypt(self.threshold, certs.len() as u8, &seed, &certs[..], output)?;
|
||||
} else {
|
||||
opgp.shard_and_encrypt(
|
||||
self.threshold,
|
||||
certs.len() as u8,
|
||||
&seed,
|
||||
&certs[..],
|
||||
std::io::stdout(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BottomsUp {
|
||||
fn handle(&self) -> Result<()> {
|
||||
let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
|
||||
let mnemonic = Mnemonic::from_array(entropy);
|
||||
let seed = mnemonic.generate_seed(None);
|
||||
|
||||
// TODO: should this allow for customizing the account index from 0? Potential for key reuse
|
||||
// errors.
|
||||
let path = paths::OPENPGP_DISASTER_RECOVERY
|
||||
.clone()
|
||||
.chain_push(DerivationIndex::new(0, true)?);
|
||||
let subkeys = [
|
||||
KeyFlags::empty().set_certification(),
|
||||
KeyFlags::empty().set_signing(),
|
||||
KeyFlags::empty()
|
||||
.set_transport_encryption()
|
||||
.set_storage_encryption(),
|
||||
KeyFlags::empty().set_authentication(),
|
||||
];
|
||||
let xprv = XPrv::new(seed)
|
||||
.expect("could not construct master key from seed")
|
||||
.derive_path(&path)?;
|
||||
let userid = UserID::from(self.user_id.as_str());
|
||||
|
||||
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
|
||||
let certfile = File::create(&self.output_cert)?;
|
||||
let mut w = Writer::new(certfile, Kind::PublicKey)?;
|
||||
cert.serialize(&mut w)?;
|
||||
w.finalize()?;
|
||||
|
||||
let opgp = OpenPGP::<DefaultTerminal>::new();
|
||||
let certs = OpenPGP::<DefaultTerminal>::discover_certs(&self.key_discovery)?;
|
||||
|
||||
let shardfile = File::create(&self.output_shardfile)?;
|
||||
opgp.shard_and_encrypt(
|
||||
self.threshold,
|
||||
certs.len() as u8,
|
||||
&entropy,
|
||||
&certs[..],
|
||||
shardfile,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct Wizard {
|
||||
#[command(subcommand)]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![doc = include_str!("../../../README.md")]
|
||||
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
|
@ -6,16 +6,21 @@ use std::process::ExitCode;
|
|||
|
||||
use clap::Parser;
|
||||
|
||||
use keyfork_bin::{Bin, ClosureBin};
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let bin = ClosureBin::new(|| {
|
||||
let opts = cli::Keyfork::parse();
|
||||
opts.command.handle(&opts)
|
||||
});
|
||||
let opts = cli::Keyfork::parse();
|
||||
|
||||
bin.main()
|
||||
if let Err(e) = opts.command.handle(&opts) {
|
||||
eprintln!("Unable to run command: {e}");
|
||||
let mut source = e.source();
|
||||
while let Some(new_error) = source.take() {
|
||||
eprintln!("Source: {new_error}");
|
||||
source = new_error.source();
|
||||
}
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "keyfork-qrcode"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
repository = "https://git.distrust.co/public/keyfork"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
@ -8,15 +8,14 @@ license = "MIT"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["bin"]
|
||||
bin = ["decode-backend-rqrr"]
|
||||
default = []
|
||||
bin = ["decode-backend-zbar"]
|
||||
decode-backend-rqrr = ["dep:rqrr"]
|
||||
decode-backend-zbar = ["dep:keyfork-zbar"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-bug = { workspace = true }
|
||||
keyfork-zbar = { workspace = true, optional = true }
|
||||
image = { workspace = true, default-features = false, features = ["jpeg"] }
|
||||
rqrr = { version = "0.7.0", optional = true }
|
||||
thiserror = { workspace = true }
|
||||
v4l = { workspace = true }
|
||||
image = { version = "0.24.7", default-features = false, features = ["jpeg"] }
|
||||
keyfork-zbar = { version = "0.1.0", path = "../keyfork-zbar", optional = true }
|
||||
rqrr = { version = "0.6.0", optional = true }
|
||||
thiserror = "1.0.56"
|
||||
v4l = "0.14.0"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#![allow(missing_docs)]
|
||||
//!
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
//! Encoding and decoding QR codes.
|
||||
|
||||
use keyfork_bug as bug;
|
||||
|
||||
use image::ImageReader;
|
||||
use image::io::Reader as ImageReader;
|
||||
use std::{
|
||||
io::{Cursor, Write},
|
||||
time::{Duration, Instant},
|
||||
time::{Duration, SystemTime},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use v4l::{
|
||||
|
@ -100,25 +98,19 @@ pub fn qrencode(
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
|
||||
|
||||
/// Continuously scan the `index`-th camera for a QR code.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The function may return an error if the hardware is unable to scan video or if an image could
|
||||
/// not be decoded.
|
||||
#[cfg(feature = "decode-backend-rqrr")]
|
||||
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||
let device = Device::new(index)?;
|
||||
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
||||
let mut fmt = device.format().expect("Failed to read format");
|
||||
fmt.fourcc = FourCC::new(b"MPG1");
|
||||
device.set_format(&fmt)?;
|
||||
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||
let start = Instant::now();
|
||||
let start = SystemTime::now();
|
||||
|
||||
while Instant::now()
|
||||
while SystemTime::now()
|
||||
.duration_since(start)
|
||||
.unwrap_or(Duration::from_secs(0))
|
||||
< timeout
|
||||
{
|
||||
let (buffer, _) = stream.next()?;
|
||||
|
@ -138,23 +130,19 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
|
|||
}
|
||||
|
||||
/// Continuously scan the `index`-th camera for a QR code.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The function may return an error if the hardware is unable to scan video or if an image could
|
||||
/// not be decoded.
|
||||
#[cfg(feature = "decode-backend-zbar")]
|
||||
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||
let device = Device::new(index)?;
|
||||
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
||||
let mut fmt = device.format().expect("Failed to read format");
|
||||
fmt.fourcc = FourCC::new(b"MPG1");
|
||||
device.set_format(&fmt)?;
|
||||
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||
let start = Instant::now();
|
||||
let start = SystemTime::now();
|
||||
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
|
||||
|
||||
while Instant::now()
|
||||
while SystemTime::now()
|
||||
.duration_since(start)
|
||||
.unwrap_or(Duration::from_secs(0))
|
||||
< timeout
|
||||
{
|
||||
let (buffer, _) = stream.next()?;
|
||||
|
|
|
@ -9,13 +9,12 @@ license = "MIT"
|
|||
|
||||
[features]
|
||||
default = ["image"]
|
||||
bin = ["image"]
|
||||
image = ["dep:image"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-zbar-sys = { workspace = true }
|
||||
image = { workspace = true, default-features = false, optional = true }
|
||||
thiserror = { workspace = true }
|
||||
image = { version = "0.24.7", default-features = false, optional = true }
|
||||
keyfork-zbar-sys = { version = "0.1.0", path = "../keyfork-zbar-sys" }
|
||||
thiserror = "1.0.56"
|
||||
|
||||
[dev-dependencies]
|
||||
v4l = { workspace = true }
|
||||
v4l = "0.14.0"
|
||||
|
|
|
@ -33,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.decode()?,
|
||||
);
|
||||
|
||||
if let Some(symbol) = scanner.scan_image(&image).first() {
|
||||
if let Some(symbol) = scanner.scan_image(&image).get(0) {
|
||||
println!("{}", String::from_utf8_lossy(symbol.data()));
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
@ -22,9 +22,9 @@ impl Image {
|
|||
///
|
||||
/// A FourCC code can be given in the format:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// ```no_run
|
||||
/// self.set_format(b"Y800")
|
||||
/// ```
|
||||
/// ````
|
||||
pub(crate) fn set_format(&mut self, fourcc: &[u8; 4]) {
|
||||
let fourcc: u64 = fourcc[0] as u64
|
||||
| ((fourcc[1] as u64) << 8)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! A Symbol represents some form of encoded data.
|
||||
//!
|
||||
|
||||
use super::sys;
|
||||
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
[package]
|
||||
name = "keyfork-bin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
|
@ -1,140 +0,0 @@
|
|||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
//! A convenient trait for quickly writing binaries in a consistent pattern.
|
||||
//!
|
||||
//! # Examples
|
||||
//! ```rust
|
||||
//! use anyhow::anyhow;
|
||||
//! use keyfork_bin::Bin;
|
||||
//!
|
||||
//! struct Main;
|
||||
//!
|
||||
//! impl Bin for Main {
|
||||
//! type Args = (String, String);
|
||||
//!
|
||||
//! fn usage_hint(&self) -> Option<String> {
|
||||
//! Some(String::from("<param1> <param2>"))
|
||||
//! }
|
||||
//!
|
||||
//! fn validate_args(&self, mut args: impl Iterator<Item = String>) -> keyfork_bin::ProcessResult<Self::Args> {
|
||||
//! let arg1 = args.next().ok_or(anyhow!("missing argument 1"))?;
|
||||
//! let arg2 = args.next().ok_or(anyhow!("missing argument 2"))?;
|
||||
//! Ok((arg1, arg2))
|
||||
//! }
|
||||
//!
|
||||
//! fn run(&self, (arg1, arg2): Self::Args) -> keyfork_bin::ProcessResult {
|
||||
//! println!("First argument: {arg1}");
|
||||
//! println!("Second argument: {arg2}");
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!#
|
||||
//!# fn main(&self) -> std::process::ExitCode {
|
||||
//!# self.main_inner([String::from("hello"), String::from("world")].into_iter())
|
||||
//!# }
|
||||
//! }
|
||||
//!
|
||||
//! fn main() {
|
||||
//! // Assume the program was called with something like "hello world"...
|
||||
//! let bin = Main;
|
||||
//! bin.main();
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
/// A result that may contain any error.
|
||||
pub type ProcessResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn report_err(e: Box<dyn std::error::Error>) {
|
||||
eprintln!("Unable to run command: {e}");
|
||||
let mut source = e.source();
|
||||
while let Some(new_error) = source.take() {
|
||||
eprintln!("- Caused by: {new_error}");
|
||||
source = new_error.source();
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for implementing the flow of a binary's execution.
|
||||
pub trait Bin {
|
||||
/// The type for command-line arguments required by the function.
|
||||
type Args;
|
||||
|
||||
/// A usage hint for how the arguments should be provided to the program.
|
||||
fn usage_hint(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Validate the arguments provided by the user into types required by the binary.
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
fn validate_args(&self, args: impl Iterator<Item = String>) -> ProcessResult<Self::Args>;
|
||||
|
||||
/// Run the binary
|
||||
#[allow(clippy::missing_errors_doc)]
|
||||
fn run(&self, args: Self::Args) -> ProcessResult;
|
||||
|
||||
/// The default handler for running the binary and reporting any errors.
|
||||
fn main(&self) -> ExitCode {
|
||||
self.main_inner(std::env::args())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn main_inner(&self, mut args: impl Iterator<Item = String>) -> ExitCode {
|
||||
let command = args.next();
|
||||
let args = match self.validate_args(args) {
|
||||
Ok(args) => args,
|
||||
Err(e) => {
|
||||
if let (Some(command), Some(hint)) = (command, self.usage_hint()) {
|
||||
eprintln!("Usage: {command} {hint}");
|
||||
}
|
||||
report_err(e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = self.run(args) {
|
||||
report_err(e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
/// A Bin that doesn't take any arguments.
|
||||
pub struct ClosureBin<F: Fn() -> ProcessResult> {
|
||||
closure: F
|
||||
}
|
||||
|
||||
impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
|
||||
/// Create a new Bin from a closure.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use keyfork_bin::{Bin, ClosureBin};
|
||||
///
|
||||
/// let bin = ClosureBin::new(|| {
|
||||
/// println!("Hello, world!");
|
||||
/// Ok(())
|
||||
/// });
|
||||
///
|
||||
/// bin.main();
|
||||
/// ```
|
||||
pub fn new(closure: F) -> Self {
|
||||
Self {
|
||||
closure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Bin for ClosureBin<F> where F: Fn() -> ProcessResult {
|
||||
type Args = ();
|
||||
|
||||
fn validate_args(&self, _args: impl Iterator<Item = String>) -> ProcessResult<Self::Args> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(&self, _args: Self::Args) -> ProcessResult {
|
||||
let c = &self.closure;
|
||||
c()
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
[package]
|
||||
name = "keyfork-bug"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
|
@ -1,108 +0,0 @@
|
|||
//! Keyfork Bug Reporting Utilities.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::{fs::File, io::Write};
|
||||
//! use keyfork_bug as bug;
|
||||
//!
|
||||
//! let option = Some("hello world!");
|
||||
//! let value = option.expect(bug::bug!("missing str value!"));
|
||||
//!
|
||||
//! let mut output_file = File::create("/dev/null").expect(bug::bug!("can't open /dev/null"));
|
||||
//! output_file
|
||||
//! .write_all(value.as_bytes())
|
||||
//! .unwrap_or_else(bug::panic!("Can't write to file: {}", value));
|
||||
//! ```
|
||||
//!
|
||||
//! ```rust,should_panic
|
||||
//! use std::fs::File;
|
||||
//! use keyfork_bug as bug;
|
||||
//!
|
||||
//! let mut output_file = File::open("/dev/nukk").expect(bug::bug!("can't open /dev/null"));
|
||||
//! ```
|
||||
|
||||
/// The mutex was poisoned and is unusable.
|
||||
pub const POISONED_MUTEX: &str = "The mutex was poisoned and is unusable";
|
||||
|
||||
/// Automatically generate a bug report message for Keyfork. This macro is intended to use when
|
||||
/// using `Result::expect()` or `Option::expect()` to retrieve information about the callsite where
|
||||
/// the bug was located.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use keyfork_bug::bug;
|
||||
///
|
||||
/// let option = Some(0u32);
|
||||
/// let value = option.expect(bug!("missing u32 value!"));
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// use keyfork_bug::bug;
|
||||
///
|
||||
/// let error_message = "This is a really long error message that should not be in the macro.";
|
||||
/// let option = Some(0u32);
|
||||
/// let value = option.expect(bug!(error_message));
|
||||
/// ```
|
||||
///
|
||||
/// ```rust,should_panic
|
||||
/// use keyfork_bug::bug;
|
||||
///
|
||||
/// let option: Option<u32> = None;
|
||||
/// let value = option.expect(bug!("missing u32 value!"));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! bug {
|
||||
($input:literal) => {
|
||||
concat!(
|
||||
"Keyfork encountered a BUG at: [",
|
||||
file!(),
|
||||
":",
|
||||
line!(),
|
||||
":",
|
||||
column!(),
|
||||
"]: ",
|
||||
$input,
|
||||
"\n\nReport this bug to <team@distrust.co>, this behavior is unexpected!"
|
||||
)
|
||||
};
|
||||
($input:ident) => {
|
||||
format!(
|
||||
concat!("Keyfork encountered a BUG at: [{file}:{line}:{column}]: {input}\n\n",
|
||||
"Report this bug to <team@distrust.co>, this behavior is unexpected!"
|
||||
),
|
||||
file=file!(),
|
||||
line=line!(),
|
||||
column=column!(),
|
||||
input=$input,
|
||||
).as_str()
|
||||
};
|
||||
($($arg:tt)*) => {{
|
||||
let message = format!($($arg)*);
|
||||
$crate::bug!(message)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Return a closure that, when called, panics with a bug report message for Keyfork. Returning a
|
||||
/// closure can help handle the `clippy::expect_fun_call` lint. The closure accepts an error
|
||||
/// argument, so it is suitable for being used with [`Result`] types instead of [`Option`] types.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use std::fs::File;
|
||||
/// use keyfork_bug as bug;
|
||||
///
|
||||
/// let file = File::open("/dev/null").unwrap_or_else(bug::panic!("couldn't open /dev/null"));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! panic {
|
||||
($input:literal) => { |e| {
|
||||
std::panic!("{}\n{}", $crate::bug!($input), e)
|
||||
}};
|
||||
($input:ident) => { |e| {
|
||||
std::panic!("{}\n{}", $crate::bug!($input), e)
|
||||
}};
|
||||
($($arg:tt)*) => { |e| {
|
||||
std::panic!("{}\n{}", $crate::bug!($($arg)*), e)
|
||||
}};
|
||||
}
|
|
@ -55,16 +55,16 @@ crossterm_winapi = { version = "0.9.1", optional = true }
|
|||
libc = "0.2"
|
||||
signal-hook = { version = "0.3.17", optional = true }
|
||||
filedescriptor = { version = "0.8", optional = true }
|
||||
mio = { version = "1.0", features = ["os-poll"], optional = true }
|
||||
signal-hook-mio = { version = "0.2.3", features = ["support-v1_0"], optional = true }
|
||||
mio = { version = "0.8", features = ["os-poll"], optional = true }
|
||||
signal-hook-mio = { version = "0.2.3", features = ["support-v0_8"], optional = true }
|
||||
|
||||
# Dev dependencies (examples, ...)
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio = { version = "1.25", features = ["full"] }
|
||||
futures = "0.3"
|
||||
futures-timer = "3.0"
|
||||
async-std = "1.12"
|
||||
serde_json = { workspace = true }
|
||||
serde_json = "1.0"
|
||||
serial_test = "2.0.0"
|
||||
|
||||
# Examples
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#![allow(missing_docs)]
|
||||
//!
|
||||
|
||||
use keyfork_crossterm::{
|
||||
execute,
|
||||
|
|
|
@ -13,7 +13,7 @@ use crate::{
|
|||
/// The top left cell is represented as `(0, 0)`.
|
||||
///
|
||||
/// On unix systems, this function will block and possibly time out while
|
||||
/// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
pub fn position() -> io::Result<(u16, u16)> {
|
||||
if is_raw_mode_enabled() {
|
||||
read_position_raw()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::{collections::VecDeque, io, time::Duration};
|
||||
|
||||
use mio::{unix::SourceFd, Events, Interest, Poll, Token};
|
||||
use signal_hook_mio::v1_0::Signals;
|
||||
use signal_hook_mio::v0_8::Signals;
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
use crate::event::sys::Waker;
|
||||
|
|
|
@ -170,7 +170,7 @@ pub fn available_color_count() -> u16 {
|
|||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// crossterm supports NO_COLOR (<https://no-color.org/>) to disabled colored output.
|
||||
/// crossterm supports NO_COLOR (https://no-color.org/) to disabled colored output.
|
||||
///
|
||||
/// This API allows applications to override that behavior and force colorized output
|
||||
/// even if NO_COLOR is set.
|
||||
|
|
|
@ -71,7 +71,7 @@ impl Colored {
|
|||
}
|
||||
|
||||
/// Checks whether ansi color sequences are disabled by setting of NO_COLOR
|
||||
/// in environment as per <https://no-color.org/>
|
||||
/// in environment as per https://no-color.org/
|
||||
pub fn ansi_color_disabled() -> bool {
|
||||
!std::env::var("NO_COLOR")
|
||||
.unwrap_or("".to_string())
|
||||
|
|
|
@ -199,7 +199,7 @@ pub struct WindowSize {
|
|||
/// Returns the terminal size `[WindowSize]`.
|
||||
///
|
||||
/// The width and height in pixels may not be reliably implemented or default to 0.
|
||||
/// For unix, <https://man7.org/linux/man-pages/man4/tty_ioctl.4.html> documents them as "unused".
|
||||
/// For unix, https://man7.org/linux/man-pages/man4/tty_ioctl.4.html documents them as "unused".
|
||||
/// For windows it is not implemented.
|
||||
pub fn window_size() -> io::Result<WindowSize> {
|
||||
sys::window_size()
|
||||
|
|
|
@ -144,7 +144,7 @@ pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
|||
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||
///
|
||||
/// On unix systems, this function will block and possibly time out while
|
||||
/// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn supports_keyboard_enhancement() -> io::Result<bool> {
|
||||
if is_raw_mode_enabled() {
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
[package]
|
||||
name = "keyfork-entropy"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["bin"]
|
||||
default = []
|
||||
bin = ["smex"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-bug = { workspace = true }
|
||||
smex = { workspace = true, optional = true }
|
||||
smex = { version = "0.1.0", path = "../smex", optional = true }
|
||||
|
|
|
@ -1,26 +1,20 @@
|
|||
//! Utilities for reading entropy from secure sources.
|
||||
|
||||
use keyfork_bug::bug;
|
||||
|
||||
use std::{
|
||||
fs::{read_dir, read_to_string, File},
|
||||
io::Read,
|
||||
};
|
||||
use std::{fs::{read_dir, read_to_string, File}, io::Read};
|
||||
|
||||
static WARNING_LINKS: [&str; 1] =
|
||||
["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"];
|
||||
|
||||
fn ensure_safe_kernel_version() {
|
||||
let kernel_version =
|
||||
read_to_string("/proc/version").expect(bug!("Unable to open file: /proc/version"));
|
||||
let kernel_version = read_to_string("/proc/version").expect("/proc/version");
|
||||
let v = kernel_version
|
||||
.split(' ')
|
||||
.nth(2)
|
||||
.expect(bug!("Unable to parse kernel version"))
|
||||
.expect("Unable to parse kernel version")
|
||||
.split('.')
|
||||
.take(2)
|
||||
.map(str::parse)
|
||||
.map(|x| x.expect(bug!("Unable to parse kernel version number")))
|
||||
.map(|x| x.expect("Unable to parse kernel version number"))
|
||||
.collect::<Vec<u32>>();
|
||||
let [major, minor, ..] = v.as_slice() else {
|
||||
panic!("Unable to determine major and minor: {kernel_version}");
|
||||
|
@ -33,49 +27,31 @@ fn ensure_safe_kernel_version() {
|
|||
}
|
||||
|
||||
fn ensure_offline() {
|
||||
let paths = read_dir("/sys/class/net").expect(bug!("Unable to read network interfaces"));
|
||||
let paths = read_dir("/sys/class/net").expect("Unable to read network interfaces");
|
||||
for entry in paths {
|
||||
let mut path = entry.expect(bug!("Unable to read directory entry")).path();
|
||||
let mut path = entry.expect("Unable to read directory entry").path();
|
||||
if path
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.expect(bug!("Unable to decode UTF-8 filepath"))
|
||||
.expect("Unable to decode UTF-8 filepath")
|
||||
.split('/')
|
||||
.last()
|
||||
.expect(bug!("No data in file path"))
|
||||
.expect("No data in file path")
|
||||
== "lo"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
path.push("operstate");
|
||||
let isup =
|
||||
read_to_string(&path).expect(bug!("Unable to read operstate of network interfaces"));
|
||||
let isup = read_to_string(&path).expect("Unable to read operstate of network interfaces");
|
||||
assert_ne!(isup.trim(), "up", "No network interfaces should be up");
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the system is safe.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
|
||||
/// keyfork_entropy::ensure_safe();
|
||||
/// ```
|
||||
///
|
||||
/// When running on a system that's online, or running an outdated kernel:
|
||||
///
|
||||
/// ```rust,should_panic
|
||||
/// # // NOTE: sometimes, the environment variable is set, for testing purposes. I'm not sure how
|
||||
/// # // to un-set it. Set it to a sentinel value.
|
||||
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "test-must-fail");
|
||||
/// # std::env::set_var("INSECURE_HARDWARE_ALLOWED", "test-must-fail");
|
||||
/// keyfork_entropy::ensure_safe();
|
||||
/// ```
|
||||
pub fn ensure_safe() {
|
||||
if !std::env::vars().any(|(name, value)| {
|
||||
(name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
|
||||
&& value != "test-must-fail"
|
||||
}) {
|
||||
if !std::env::vars()
|
||||
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
|
||||
{
|
||||
ensure_safe_kernel_version();
|
||||
ensure_offline();
|
||||
}
|
||||
|
@ -85,16 +61,6 @@ pub fn ensure_safe() {
|
|||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if an error occurred while reading from the random source.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust,no_run
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
|
||||
/// let entropy = keyfork_entropy::generate_entropy_of_size(64)?;
|
||||
/// assert_eq!(entropy.len(), 64);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::Error> {
|
||||
ensure_safe();
|
||||
let mut vec = vec![0u8; byte_count];
|
||||
|
@ -102,24 +68,3 @@ pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::E
|
|||
entropy_file.read_exact(&mut vec[..])?;
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
/// Read system entropy of a constant size.
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if an error occurred while reading from the random source.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust,no_run
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
|
||||
/// let entropy = keyfork_entropy::generate_entropy_of_const_size::<64>()?;
|
||||
/// assert_eq!(entropy.len(), 64);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn generate_entropy_of_const_size<const N: usize>() -> Result<[u8; N], std::io::Error> {
|
||||
let mut output = [0u8; N];
|
||||
let mut entropy_file = File::open("/dev/urandom")?;
|
||||
entropy_file.read_exact(&mut output[..])?;
|
||||
Ok(output)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! Generate entropy of a given size, encoded as hex.
|
||||
//!
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bit_size: usize = std::env::args()
|
||||
|
@ -10,15 +10,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
bit_size % 8 == 0,
|
||||
"Bit size must be divisible by 8, got: {bit_size}"
|
||||
);
|
||||
match bit_size {
|
||||
128 | 256 | 512 => {}
|
||||
_ => {
|
||||
eprintln!("reading entropy of uncommon size: {bit_size}");
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
bit_size <= 256,
|
||||
"Maximum supported bit size is 256, got: {bit_size}"
|
||||
);
|
||||
|
||||
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
|
||||
println!("{}", smex::encode(entropy));
|
||||
println!("{}", smex::encode(&entropy));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -12,13 +12,13 @@ async = ["dep:tokio"]
|
|||
|
||||
[dependencies]
|
||||
# Included in Rust
|
||||
sha2 = { workspace = true }
|
||||
sha2 = "0.10.7"
|
||||
|
||||
# Personally audited
|
||||
thiserror = { workspace = true }
|
||||
thiserror = "1.0.47"
|
||||
|
||||
# Optional, not personally audited
|
||||
tokio = { workspace = true, optional = true, features = ["io-util"] }
|
||||
tokio = { version = "1.32.0", optional = true, features = ["io-util"] }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.31.0"
|
||||
|
|
|
@ -66,11 +66,6 @@ pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
|
|||
/// # Errors
|
||||
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
|
||||
/// constraint on a protocol level.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// let data = keyfork_frame::try_encode(b"hello world!".as_slice()).unwrap();
|
||||
/// ```
|
||||
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
|
||||
let mut output = vec![];
|
||||
try_encode_to(data, &mut output)?;
|
||||
|
@ -82,12 +77,6 @@ pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
|
|||
/// # Errors
|
||||
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
|
||||
/// is unable to write data.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// let mut output = vec![];
|
||||
/// keyfork_frame::try_encode_to(b"hello world!".as_slice(), &mut output).unwrap();
|
||||
/// ```
|
||||
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
|
||||
let hash = hash(data);
|
||||
let len = hash.len() + data.len();
|
||||
|
@ -118,40 +107,18 @@ pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> {
|
|||
/// * The given `data` does not contain enough data to parse a length,
|
||||
/// * The given `data` does not contain the given length's worth of data,
|
||||
/// * The given `data` has a checksum that does not match what we build locally.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// let input = b"hello world!";
|
||||
/// let encoded = keyfork_frame::try_encode(input.as_slice()).unwrap();
|
||||
/// let decoded = keyfork_frame::try_decode(&encoded).unwrap();
|
||||
/// assert_eq!(input.as_slice(), decoded.as_slice());
|
||||
/// ```
|
||||
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
|
||||
try_decode_from(&mut &data[..])
|
||||
}
|
||||
|
||||
/// Read and decode a framed message into a `Vec<u8>`.
|
||||
///
|
||||
/// Note that unlike [`try_encode_to`], this method does not allow writing to an object
|
||||
/// implementing Write. This is because the data must be stored entirely in memory to allow
|
||||
/// verifying the data. The data is then returned using the same in-memory representation as is
|
||||
/// used in memory, and a caller may then choose to use `writable.write_all()`.
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if:
|
||||
/// * The given `data` does not contain enough data to parse a length,
|
||||
/// * The given `data` does not contain the given length's worth of data,
|
||||
/// * The given `data` has a checksum that does not match what we build locally.
|
||||
/// * The source for the data returned an error.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// let input = b"hello world!";
|
||||
/// let mut encoded = vec![];
|
||||
/// keyfork_frame::try_encode_to(input.as_slice(), &mut encoded).unwrap();
|
||||
/// let decoded = keyfork_frame::try_decode_from(&mut &encoded[..]).unwrap();
|
||||
/// assert_eq!(input.as_slice(), decoded.as_slice());
|
||||
/// ```
|
||||
pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
|
||||
let mut bytes = 0u32.to_be_bytes();
|
||||
readable.read_exact(&mut bytes)?;
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
[package]
|
||||
name = "keyfork-mnemonic"
|
||||
version = "0.4.0"
|
||||
name = "keyfork-mnemonic-util"
|
||||
version = "0.1.0"
|
||||
description = "Utilities to generate and manage seeds based on BIP-0039 mnemonics."
|
||||
repository = "https://git.distrust.co/public/keyfork"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[features]
|
||||
default = ["bin"]
|
||||
default = []
|
||||
bin = ["smex"]
|
||||
|
||||
[dependencies]
|
||||
smex = { workspace = true, optional = true }
|
||||
keyfork-bug = { workspace = true }
|
||||
# Included in rust
|
||||
sha2 = "0.10.7"
|
||||
|
||||
sha2 = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
hmac = "0.12.1"
|
||||
pbkdf2 = "0.12.2"
|
||||
smex = { version = "0.1.0", path = "../smex", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
bip39 = "2.0.0"
|
||||
hex = "0.4.3"
|
||||
serde_json = { workspace = true }
|
||||
serde_json = "1.0.105"
|
|
@ -1,6 +1,6 @@
|
|||
//! Generate a mnemonic from hex-encoded input.
|
||||
//!
|
||||
|
||||
use keyfork_mnemonic::Mnemonic;
|
||||
use keyfork_mnemonic_util::Mnemonic;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let input = std::io::stdin();
|
||||
|
@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
input.read_line(&mut line)?;
|
||||
let decoded = smex::decode(line.trim())?;
|
||||
|
||||
let mnemonic = Mnemonic::from_raw_bytes(&decoded) ;
|
||||
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&decoded, Default::default()) };
|
||||
|
||||
println!("{mnemonic}");
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
//! Zero-dependency Mnemonic encoding and decoding.
|
||||
|
||||
use std::{error::Error, fmt::Display, str::FromStr, sync::Arc};
|
||||
|
||||
use hmac::Hmac;
|
||||
use pbkdf2::pbkdf2;
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
|
||||
/// The error type representing a failure to create a [`Mnemonic`]. These errors only occur during
|
||||
/// [`Mnemonic`] creation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MnemonicGenerationError {
|
||||
/// The amount of bits passed to a mnemonic must be divisible by 32.
|
||||
InvalidByteCount(usize),
|
||||
|
||||
/// The length of a mnemonic in bits must be within the BIP-0039 range, and supported by the
|
||||
/// library. Currently, only 128, 192 (for testing purposes), and 256 are supported.
|
||||
InvalidByteLength(usize),
|
||||
|
||||
/// Invalid length resulting from PBKDF2.
|
||||
InvalidPbkdf2Length,
|
||||
}
|
||||
|
||||
impl Display for MnemonicGenerationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MnemonicGenerationError::InvalidByteCount(count) => {
|
||||
write!(f, "Invalid byte count: {count}, must be divisible by 8")
|
||||
}
|
||||
MnemonicGenerationError::InvalidByteLength(count) => {
|
||||
write!(f, "Invalid byte length: {count}, must be 128 or 256")
|
||||
}
|
||||
MnemonicGenerationError::InvalidPbkdf2Length => {
|
||||
f.write_str("Invalid length from PBKDF2")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MnemonicGenerationError {}
|
||||
|
||||
/// A BIP-0039 compatible list of words.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Wordlist(Vec<String>);
|
||||
|
||||
impl Default for Wordlist {
|
||||
/// Returns the English wordlist in the Bitcoin BIP-0039 specification.
|
||||
fn default() -> Self {
|
||||
// TODO: English is the only supported language.
|
||||
let wordlist_file = include_str!("data/wordlist.txt");
|
||||
Wordlist(
|
||||
wordlist_file
|
||||
.lines()
|
||||
// skip 1: comment at top of file to point to BIP-0039 source.
|
||||
.skip(1)
|
||||
.map(|x| x.trim().to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Wordlist {
|
||||
/// Return an Arced version of the Wordlist
|
||||
#[allow(clippy::must_use_candidate)]
|
||||
pub fn arc(self) -> Arc<Self> {
|
||||
Arc::new(self)
|
||||
}
|
||||
|
||||
/// Determine whether the Wordlist contains a given word.
|
||||
pub fn contains(&self, word: &str) -> bool {
|
||||
self.0.iter().any(|w| w.as_str() == word)
|
||||
}
|
||||
|
||||
/// Given an index, get a word from the wordlist.
|
||||
pub fn get_word(&self, word: usize) -> Option<&String> {
|
||||
self.0.get(word)
|
||||
}
|
||||
|
||||
/*
|
||||
fn inner(&self) -> &Vec<String> {
|
||||
&self.0
|
||||
}
|
||||
*/
|
||||
|
||||
#[cfg(test)]
|
||||
fn into_inner(self) -> Vec<String> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A BIP-0039 mnemonic with reference to a [`Wordlist`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mnemonic {
|
||||
entropy: Vec<u8>,
|
||||
// words: Vec<usize>,
|
||||
wordlist: Arc<Wordlist>,
|
||||
}
|
||||
|
||||
impl Display for Mnemonic {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let bit_count = self.entropy.len() * 8;
|
||||
let mut bits = vec![false; bit_count + bit_count / 32];
|
||||
|
||||
for byte_index in 0..bit_count / 8 {
|
||||
for bit_index in 0..8 {
|
||||
bits[byte_index * 8 + bit_index] =
|
||||
(self.entropy[byte_index] & (1 << (7 - bit_index))) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.entropy);
|
||||
let hash = hasher.finalize().to_vec();
|
||||
for check_bit in 0..bit_count / 32 {
|
||||
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
|
||||
}
|
||||
|
||||
let mut iter = bits
|
||||
.chunks_exact(11)
|
||||
.peekable()
|
||||
.map(|chunk| {
|
||||
let mut num = 0usize;
|
||||
for i in 0..11 {
|
||||
num += usize::from(chunk[10 - i]) << i;
|
||||
}
|
||||
num
|
||||
})
|
||||
.filter_map(|word| self.wordlist.get_word(word))
|
||||
.peekable();
|
||||
while let Some(word) = iter.next() {
|
||||
f.write_str(word)?;
|
||||
if iter.peek().is_some() {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The error type representing a failure to parse a [`Mnemonic`]. These errors only occur during
|
||||
/// [`Mnemonic`] creation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MnemonicFromStrError {
|
||||
/// The amount of words used to parse a mnemonic was not correct.
|
||||
InvalidWordCount(usize),
|
||||
|
||||
/// One of the words used to generate the mnemonic was not found in the default wordlist.
|
||||
InvalidWord(usize),
|
||||
|
||||
/// The checksum for the mnemonic did not match the given words.
|
||||
InvalidChecksum,
|
||||
}
|
||||
|
||||
impl Display for MnemonicFromStrError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("Mnemonic error: ")?;
|
||||
match self {
|
||||
MnemonicFromStrError::InvalidWordCount(count) => {
|
||||
write!(f, "Incorrect word count: {count}")
|
||||
}
|
||||
MnemonicFromStrError::InvalidWord(index) => {
|
||||
write!(f, "Unknown word at index: {index}")
|
||||
}
|
||||
MnemonicFromStrError::InvalidChecksum => {
|
||||
f.write_str("Checksum of data did not match expected value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MnemonicFromStrError {}
|
||||
|
||||
impl FromStr for Mnemonic {
|
||||
type Err = MnemonicFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let words: Vec<_> = s.split_whitespace().collect();
|
||||
let mut usize_words = vec![];
|
||||
let wordlist = Wordlist::default().arc();
|
||||
let mut bits = vec![false; words.len() * 11];
|
||||
for (index, word) in words.iter().enumerate() {
|
||||
let word = wordlist
|
||||
.0
|
||||
.iter()
|
||||
.position(|w| w == word)
|
||||
.ok_or(MnemonicFromStrError::InvalidWord(index))?;
|
||||
usize_words.push(word);
|
||||
for bit in 0..11 {
|
||||
bits[index * 11 + bit] = (word & (1 << (10 - bit))) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
let mut checksum_bits = vec![false; bits.len() - (bits.len() * 32 / 33)];
|
||||
checksum_bits.copy_from_slice(&bits[bits.len() * 32 / 33..]);
|
||||
|
||||
// remove checksum bits
|
||||
bits.truncate(bits.len() * 32 / 33);
|
||||
// bits.truncate(bits.len() - bits.len() % 32);
|
||||
|
||||
let entropy: Vec<u8> = bits
|
||||
.chunks_exact(8)
|
||||
.map(|chunk| {
|
||||
let mut num = 0u8;
|
||||
for i in 0..8 {
|
||||
num += u8::from(chunk[7 - i]) << i;
|
||||
}
|
||||
num
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&entropy);
|
||||
let hash = hasher.finalize().to_vec();
|
||||
|
||||
for (i, bit) in checksum_bits.iter().enumerate() {
|
||||
if !hash[i / 8] & (1 << (7 - (i % 8))) == u8::from(*bit) {
|
||||
return Err(MnemonicFromStrError::InvalidChecksum);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Mnemonic {
|
||||
entropy,
|
||||
// words: usize_words,
|
||||
wordlist,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Mnemonic {
|
||||
/// Generate a [`Mnemonic`] from the provided entropy and [`Wordlist`].
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if the entropy is not within the acceptable lengths.
|
||||
pub fn from_entropy(
|
||||
bytes: &[u8],
|
||||
wordlist: Arc<Wordlist>,
|
||||
) -> Result<Mnemonic, MnemonicGenerationError> {
|
||||
let bit_count = bytes.len() * 8;
|
||||
|
||||
if bit_count % 32 != 0 {
|
||||
return Err(MnemonicGenerationError::InvalidByteCount(bit_count));
|
||||
}
|
||||
// 192 supported for test suite
|
||||
if ![128, 192, 256].contains(&bit_count) {
|
||||
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
|
||||
}
|
||||
|
||||
Ok(unsafe { Self::from_raw_entropy(bytes, wordlist) })
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
||||
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
||||
/// == 0`.
|
||||
pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic {
|
||||
Mnemonic {
|
||||
entropy: bytes.to_vec(),
|
||||
wordlist,
|
||||
}
|
||||
}
|
||||
|
||||
/// The internal representation of the decoded data.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.entropy
|
||||
}
|
||||
|
||||
/// The internal representation of the decoded data, as a [`Vec<u8>`].
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.entropy.to_vec()
|
||||
}
|
||||
|
||||
/// Drop self, returning the decoded data.
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.entropy
|
||||
}
|
||||
|
||||
/// Clone the existing entropy.
|
||||
#[deprecated]
|
||||
pub fn entropy(&self) -> Vec<u8> {
|
||||
self.entropy.clone()
|
||||
}
|
||||
|
||||
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the pbkdf2 function returns an invalid length, but this
|
||||
/// case should not be reached.
|
||||
pub fn seed<'a>(
|
||||
&self,
|
||||
passphrase: impl Into<Option<&'a str>>,
|
||||
) -> Result<Vec<u8>, MnemonicGenerationError> {
|
||||
let passphrase = passphrase.into();
|
||||
|
||||
let mut seed = [0u8; 64];
|
||||
let mnemonic = self.to_string();
|
||||
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
|
||||
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
|
||||
.map_err(|_| MnemonicGenerationError::InvalidPbkdf2Length)?;
|
||||
Ok(seed.to_vec())
|
||||
}
|
||||
|
||||
/// Encode the mnemonic into a list of wordlist indexes.
|
||||
pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) {
|
||||
let bit_count = self.entropy.len() * 8;
|
||||
let mut bits = vec![false; bit_count + bit_count / 32];
|
||||
|
||||
for byte_index in 0..bit_count / 8 {
|
||||
for bit_index in 0..8 {
|
||||
bits[byte_index * 8 + bit_index] =
|
||||
(self.entropy[byte_index] & (1 << (7 - bit_index))) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.entropy);
|
||||
let hash = hasher.finalize().to_vec();
|
||||
for check_bit in 0..bit_count / 32 {
|
||||
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
|
||||
}
|
||||
|
||||
let words = bits.chunks_exact(11).peekable().map(|chunk| {
|
||||
let mut num = 0usize;
|
||||
for i in 0..11 {
|
||||
num += usize::from(chunk[10 - i]) << i;
|
||||
}
|
||||
num
|
||||
});
|
||||
(words.collect(), self.wordlist.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashSet, fs::File, io::Read};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wordlist_word_count_correct() {
|
||||
let wordlist = Wordlist::default().into_inner();
|
||||
assert_eq!(
|
||||
wordlist.len(),
|
||||
2usize.pow(11),
|
||||
"Wordlist did not include correct word count"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reproduces_its_own_seed() {
|
||||
let mut random_handle = File::open("/dev/random").unwrap();
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
random_handle.read_exact(&mut entropy[..]).unwrap();
|
||||
let wordlist = Wordlist::default().arc();
|
||||
let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
|
||||
let new_entropy = mnemonic.entropy();
|
||||
assert_eq!(&new_entropy, entropy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conforms_to_trezor_tests() {
|
||||
let content = include_str!("data/vectors.json");
|
||||
let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap();
|
||||
let wordlist = Wordlist::default().arc();
|
||||
|
||||
for test in jsonobj["english"].as_array().unwrap() {
|
||||
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else {
|
||||
panic!("bad test: {test}");
|
||||
};
|
||||
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
|
||||
|
||||
let mnemonic = Mnemonic::from_entropy(&hex, wordlist.clone()).unwrap();
|
||||
|
||||
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_bip39_crate() {
|
||||
let mut random_handle = File::open("/dev/random").unwrap();
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
random_handle.read_exact(&mut entropy[..]).unwrap();
|
||||
let wordlist = Wordlist::default().arc();
|
||||
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
|
||||
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
|
||||
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
|
||||
assert_eq!(my_mnemonic.seed(None).unwrap(), their_mnemonic.to_seed(""));
|
||||
assert_eq!(
|
||||
my_mnemonic.seed("testing").unwrap(),
|
||||
their_mnemonic.to_seed("testing")
|
||||
);
|
||||
assert_ne!(
|
||||
my_mnemonic.seed("test1").unwrap(),
|
||||
their_mnemonic.to_seed("test2")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_rate_of_duplicate_words() {
|
||||
let tests = 100_000;
|
||||
let mut count = 0.;
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
let wordlist = Wordlist::default().arc();
|
||||
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||
let mut hs = HashSet::<usize>::with_capacity(24);
|
||||
|
||||
for _ in 0..tests {
|
||||
random.read_exact(&mut entropy[..]).unwrap();
|
||||
let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).unwrap();
|
||||
let (words, _) = mnemonic.words();
|
||||
hs.clear();
|
||||
hs.extend(words);
|
||||
if hs.len() != 24 {
|
||||
count += 1.;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Birthday problem math is: 0.126532
|
||||
// Set values to (about) 1 below, 1 above
|
||||
// Source: https://en.wikipedia.org/wiki/Birthday_problem
|
||||
let min = 11.5;
|
||||
let max = 13.5;
|
||||
assert!(
|
||||
count > f64::from(tests) * min / 100.,
|
||||
"{count} probability should be more than {min}%: {}",
|
||||
count / f64::from(tests)
|
||||
);
|
||||
assert!(
|
||||
count < f64::from(tests) * max / 100.,
|
||||
"{count} probability should be more than {max}%: {}",
|
||||
count / f64::from(tests)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_do_up_to_1024_bits() {
|
||||
let entropy = &mut [0u8; 128];
|
||||
let wordlist = Wordlist::default().arc();
|
||||
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||
random.read_exact(&mut entropy[..]).unwrap();
|
||||
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&entropy[..], wordlist.clone()) };
|
||||
let (words, _) = mnemonic.words();
|
||||
assert!(words.len() == 96);
|
||||
}
|
||||
}
|
|
@ -1,622 +0,0 @@
|
|||
//! Zero-dependency mnemonic encoding and decoding of data.
|
||||
//!
|
||||
//! Mnemonics can be used to safely encode data of 32, 48, and 64 bytes as a phrase:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use keyfork_mnemonic::Mnemonic;
|
||||
//! let data = b"Hello, world! I am a mnemonic :)";
|
||||
//! assert_eq!(data.len(), 32);
|
||||
//! let mnemonic = Mnemonic::try_from_slice(data).unwrap();
|
||||
//! println!("Our mnemonic is: {mnemonic}");
|
||||
//! ```
|
||||
//!
|
||||
//! A mnemonic can also be parsed from a string:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use keyfork_mnemonic::Mnemonic;
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! let data = b"Hello, world! I am a mnemonic :)";
|
||||
//! let words = "embody clock brand tattoo search desert saddle eternal
|
||||
//! goddess animal banner dolphin bitter mother loyal asset
|
||||
//! hover clock forward system normal mosquito trim credit";
|
||||
//! let mnemonic = Mnemonic::from_str(words).unwrap();
|
||||
//! assert_eq!(&data[..], mnemonic.as_bytes());
|
||||
//! ```
|
||||
//!
|
||||
//! Mnemonics can also be used to store data of other lengths, but such functionality is not
|
||||
//! verified to be safe:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use keyfork_mnemonic::Mnemonic;
|
||||
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
||||
//! let mnemonic_text = mnemonic.to_string();
|
||||
//! ```
|
||||
//!
|
||||
//! If given an invalid length, undefined behavior may follow, or code may panic.
|
||||
//!
|
||||
//! ```rust,should_panic
|
||||
//! use keyfork_mnemonic::Mnemonic;
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! // NOTE: Data is of invalid length, 31
|
||||
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
||||
//! let mnemonic_text = mnemonic.to_string();
|
||||
//! // NOTE: panic happens here
|
||||
//! let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
|
||||
//! ```
|
||||
|
||||
use std::{error::Error, fmt::Display, marker::PhantomData, str::FromStr, sync::OnceLock};
|
||||
|
||||
use keyfork_bug::bug;
|
||||
|
||||
use hmac::Hmac;
|
||||
use pbkdf2::pbkdf2;
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
|
||||
/// The error type representing a failure to create a [`Mnemonic`]. These errors only occur during
|
||||
/// [`Mnemonic`] creation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MnemonicGenerationError {
|
||||
/// The amount of bits passed to a mnemonic must be divisible by 32.
|
||||
InvalidByteCount(usize),
|
||||
|
||||
/// The length of a mnemonic in bits must be within the BIP-0039 range, and supported by the
|
||||
/// library. Currently, only 128, 192 (for testing purposes), and 256 are supported.
|
||||
InvalidByteLength(usize),
|
||||
|
||||
/// Invalid length resulting from PBKDF2.
|
||||
InvalidPbkdf2Length,
|
||||
}
|
||||
|
||||
impl Display for MnemonicGenerationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MnemonicGenerationError::InvalidByteCount(count) => {
|
||||
write!(f, "Invalid byte count: {count}, must be divisible by 8")
|
||||
}
|
||||
MnemonicGenerationError::InvalidByteLength(count) => {
|
||||
write!(f, "Invalid byte length: {count}, must be 128 or 256")
|
||||
}
|
||||
MnemonicGenerationError::InvalidPbkdf2Length => {
|
||||
f.write_str("Invalid length from PBKDF2")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MnemonicGenerationError {}
|
||||
|
||||
/// A trait representing a BIP-0039 wordlist, of 2048 words, with each word having a unique first
|
||||
/// three letters.
|
||||
pub trait Wordlist: std::fmt::Debug {
|
||||
/// Get a reference to a [`std::sync::OnceLock`] Self.
|
||||
fn get_singleton<'a>() -> &'a Self;
|
||||
|
||||
/// Return a representation of the words in the wordlist as an array of [`str`].
|
||||
fn to_str_array(&self) -> [&str; 2048];
|
||||
}
|
||||
|
||||
/// A wordlist for the English language, from the BIP-0039 dataset.
|
||||
#[derive(Debug)]
|
||||
pub struct English {
|
||||
words: [String; 2048],
|
||||
}
|
||||
|
||||
static ENGLISH: OnceLock<English> = OnceLock::new();
|
||||
|
||||
impl Wordlist for English {
|
||||
fn get_singleton<'a>() -> &'a Self {
|
||||
ENGLISH.get_or_init(|| {
|
||||
let wordlist_file = include_str!("data/wordlist.txt");
|
||||
let mut words = wordlist_file.lines().skip(1).map(|x| x.trim().to_string());
|
||||
English {
|
||||
words: std::array::from_fn(|_| {
|
||||
words.next().expect(bug!("wordlist {} should have 2048 words"))
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn to_str_array(&self) -> [&str; 2048] {
|
||||
std::array::from_fn(|i| self.words[i].as_str())
|
||||
}
|
||||
}
|
||||
|
||||
struct AssertValidMnemonicSize<const N: usize>;
|
||||
|
||||
impl<const N: usize> AssertValidMnemonicSize<N> {
|
||||
const OK_CHUNKS: () = assert!(N % 4 == 0, "bytes must be a length divisible by 4");
|
||||
const OK_SIZE: () = assert!(N <= 1024, "bytes must be less-or-equal 1024");
|
||||
}
|
||||
|
||||
/// A BIP-0039 mnemonic with reference to a [`Wordlist`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MnemonicBase<W: Wordlist> {
|
||||
data: Vec<u8>,
|
||||
marker: PhantomData<W>,
|
||||
}
|
||||
|
||||
/// A default Mnemonic using the English language.
|
||||
pub type Mnemonic = MnemonicBase<English>;
|
||||
|
||||
impl<W> Display for MnemonicBase<W>
|
||||
where
|
||||
W: Wordlist,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let wordlist = W::get_singleton();
|
||||
let words = wordlist.to_str_array();
|
||||
|
||||
let mut iter = self
|
||||
.words()
|
||||
.into_iter()
|
||||
.filter_map(|word| words.get(word))
|
||||
.peekable();
|
||||
while let Some(word) = iter.next() {
|
||||
f.write_str(word)?;
|
||||
if iter.peek().is_some() {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The error type representing a failure to parse a [`Mnemonic`]. These errors only occur during
|
||||
/// [`Mnemonic`] creation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MnemonicFromStrError {
|
||||
/// The amount of words used to parse a mnemonic was not correct.
|
||||
InvalidWordCount(usize),
|
||||
|
||||
/// One of the words used to generate the mnemonic was not found in the default wordlist.
|
||||
InvalidWord(usize),
|
||||
|
||||
/// The checksum for the mnemonic did not match the given words.
|
||||
InvalidChecksum,
|
||||
}
|
||||
|
||||
impl Display for MnemonicFromStrError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("Mnemonic error: ")?;
|
||||
match self {
|
||||
MnemonicFromStrError::InvalidWordCount(count) => {
|
||||
write!(f, "Incorrect word count: {count}")
|
||||
}
|
||||
MnemonicFromStrError::InvalidWord(index) => {
|
||||
write!(f, "Unknown word at index: {index}")
|
||||
}
|
||||
MnemonicFromStrError::InvalidChecksum => {
|
||||
f.write_str("Checksum of data did not match expected value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MnemonicFromStrError {}
|
||||
|
||||
impl<W> FromStr for MnemonicBase<W>
|
||||
where
|
||||
W: Wordlist,
|
||||
{
|
||||
type Err = MnemonicFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let wordlist = W::get_singleton();
|
||||
let wordlist_words = wordlist.to_str_array();
|
||||
let words: Vec<_> = s.split_whitespace().collect();
|
||||
let mut usize_words = vec![];
|
||||
let mut bits = vec![false; words.len() * 11];
|
||||
for (index, word) in words.iter().enumerate() {
|
||||
let word = wordlist_words
|
||||
.iter()
|
||||
.position(|w| w == word)
|
||||
.ok_or(MnemonicFromStrError::InvalidWord(index))?;
|
||||
usize_words.push(word);
|
||||
for bit in 0..11 {
|
||||
bits[index * 11 + bit] = (word & (1 << (10 - bit))) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
let mut checksum_bits = vec![false; bits.len() - (bits.len() * 32 / 33)];
|
||||
checksum_bits.copy_from_slice(&bits[bits.len() * 32 / 33..]);
|
||||
|
||||
// remove checksum bits
|
||||
bits.truncate(bits.len() * 32 / 33);
|
||||
// bits.truncate(bits.len() - bits.len() % 32);
|
||||
|
||||
let data: Vec<u8> = bits
|
||||
.chunks_exact(8)
|
||||
.map(|chunk| {
|
||||
let mut num = 0u8;
|
||||
for i in 0..8 {
|
||||
num += u8::from(chunk[7 - i]) << i;
|
||||
}
|
||||
num
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&data);
|
||||
let hash = hasher.finalize().to_vec();
|
||||
|
||||
for (i, bit) in checksum_bits.iter().enumerate() {
|
||||
if !hash[i / 8] & (1 << (7 - (i % 8))) == u8::from(*bit) {
|
||||
return Err(MnemonicFromStrError::InvalidChecksum);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MnemonicBase {
|
||||
data,
|
||||
marker: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> MnemonicBase<W>
|
||||
where
|
||||
W: Wordlist,
|
||||
{
|
||||
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
|
||||
/// of 128, 192, or 256 bits, as per BIP-0039.
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if the data is not within the expected lengths.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use keyfork_mnemonic::Mnemonic;
|
||||
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let mnemonic = Mnemonic::try_from_slice(data.as_slice()).unwrap();
|
||||
/// ```
|
||||
pub fn try_from_slice(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
|
||||
let bit_count = bytes.len() * 8;
|
||||
|
||||
if bit_count % 32 != 0 {
|
||||
return Err(MnemonicGenerationError::InvalidByteCount(bit_count));
|
||||
}
|
||||
// 192 supported for test suite
|
||||
if ![128, 192, 256].contains(&bit_count) {
|
||||
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
|
||||
}
|
||||
|
||||
Ok( Self::from_raw_bytes(bytes) )
|
||||
}
|
||||
|
||||
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data may be of a size
|
||||
/// of a factor of 4, up to 1024 bytes.
|
||||
///
|
||||
/// ```rust
|
||||
/// use keyfork_mnemonic::Mnemonic;
|
||||
/// let data = b"hello world!";
|
||||
/// let mnemonic = Mnemonic::from_array(*data);
|
||||
/// ```
|
||||
///
|
||||
/// If an invalid size is requested, the code will fail to compile:
|
||||
///
|
||||
/// ```rust,compile_fail
|
||||
/// use keyfork_mnemonic::Mnemonic;
|
||||
/// let mnemonic = Mnemonic::from_array([0u8; 53]);
|
||||
/// ```
|
||||
///
|
||||
/// ```rust,compile_fail
|
||||
/// use keyfork_mnemonic::Mnemonic;
|
||||
/// let mnemonic = Mnemonic::from_array([0u8; 1024 + 4]);
|
||||
/// ```
|
||||
pub fn from_array<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
|
||||
#[allow(clippy::let_unit_value)]
|
||||
{
|
||||
let () = AssertValidMnemonicSize::<N>::OK_CHUNKS;
|
||||
let () = AssertValidMnemonicSize::<N>::OK_SIZE;
|
||||
}
|
||||
Self::from_raw_bytes(&bytes)
|
||||
}
|
||||
|
||||
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
|
||||
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
|
||||
///
|
||||
/// # Panics
|
||||
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
||||
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
||||
/// == 0`. If the assumption is incorrect, code may panic. The
|
||||
/// [`MnemonicBase::from_array`] function may be used to generate entropy if the length of the
|
||||
/// data is known at compile-time.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use keyfork_mnemonic::Mnemonic;
|
||||
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
||||
/// let mnemonic_text = mnemonic.to_string();
|
||||
/// ```
|
||||
///
|
||||
/// If given an invalid length, undefined behavior may follow, or code may panic.
|
||||
///
|
||||
/// ```rust,should_panic
|
||||
/// use keyfork_mnemonic::Mnemonic;
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// // NOTE: Data is of invalid length, 31
|
||||
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
||||
/// ```
|
||||
pub fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
|
||||
assert!(bytes.len() % 4 == 0);
|
||||
assert!(bytes.len() <= 1024);
|
||||
MnemonicBase {
|
||||
data: bytes.to_vec(),
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
|
||||
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
||||
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
||||
/// == 0`. If the assumption is incorrect, code may panic.
|
||||
#[deprecated = "use Mnemonic::from_raw_bytes"]
|
||||
pub unsafe fn from_raw_entropy(bytes: &[u8]) -> MnemonicBase<W> {
|
||||
MnemonicBase {
|
||||
data: bytes.to_vec(),
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// A view to internal representation of the decoded data.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// A view to internal representation of the decoded data.
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// A clone of the internal representation of the decoded data.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.data.to_vec()
|
||||
}
|
||||
|
||||
/// A clone of the internal representation of the decoded data.
|
||||
pub fn to_vec(&self) -> Vec<u8> {
|
||||
self.data.to_vec()
|
||||
}
|
||||
|
||||
/// Conver the Mnemonic into the internal representation of the decoded data.
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.data
|
||||
}
|
||||
|
||||
/// Conver the Mnemonic into the internal representation of the decoded data.
|
||||
pub fn into_vec(self) -> Vec<u8> {
|
||||
self.data
|
||||
}
|
||||
|
||||
/// Clone the existing data.
|
||||
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
|
||||
pub fn entropy(&self) -> Vec<u8> {
|
||||
self.data.clone()
|
||||
}
|
||||
|
||||
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method should not return an error.
|
||||
#[deprecated = "Use generate_seed() instead"]
|
||||
pub fn seed<'a>(
|
||||
&self,
|
||||
passphrase: impl Into<Option<&'a str>>,
|
||||
) -> Result<Vec<u8>, MnemonicGenerationError> {
|
||||
Ok(self.generate_seed(passphrase).to_vec())
|
||||
}
|
||||
|
||||
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
|
||||
///
|
||||
/// # Panics
|
||||
/// The function may panic if the HmacSha512 function returns an error. The only error the
|
||||
/// HmacSha512 function should return is an invalid length, which should not be possible.
|
||||
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> [u8; 64] {
|
||||
let passphrase = passphrase.into();
|
||||
|
||||
let mut seed = [0u8; 64];
|
||||
let mnemonic = self.to_string();
|
||||
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
|
||||
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
|
||||
.expect(bug!("HmacSha512 InvalidLength should be infallible"));
|
||||
seed
|
||||
}
|
||||
|
||||
/// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a
|
||||
/// BIP-0039 wordlist.
|
||||
pub fn words(&self) -> Vec<usize> {
|
||||
let bit_count = self.data.len() * 8;
|
||||
let mut bits = vec![false; bit_count + bit_count / 32];
|
||||
|
||||
for byte_index in 0..bit_count / 8 {
|
||||
for bit_index in 0..8 {
|
||||
bits[byte_index * 8 + bit_index] =
|
||||
(self.data[byte_index] & (1 << (7 - bit_index))) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.data);
|
||||
let hash = hasher.finalize().to_vec();
|
||||
for check_bit in 0..bit_count / 32 {
|
||||
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
|
||||
}
|
||||
|
||||
// TODO: find a way to not have to collect to vec
|
||||
bits.chunks_exact(11)
|
||||
.peekable()
|
||||
.map(|chunk| {
|
||||
let mut num = 0usize;
|
||||
for i in 0..11 {
|
||||
num += usize::from(chunk[10 - i]) << i;
|
||||
}
|
||||
num
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> MnemonicBase<W>
|
||||
where
|
||||
W: Wordlist,
|
||||
{
|
||||
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
|
||||
/// of 128, 192, or 256 bits, as per BIP-0039.
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if the data is not within the expected lengths.
|
||||
#[deprecated = "use Mnemonic::try_from_slice"]
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
|
||||
MnemonicBase::try_from_slice(bytes)
|
||||
}
|
||||
|
||||
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
|
||||
/// of 128, 192, or 256 bits, as per BIP-0039.
|
||||
///
|
||||
/// # Errors
|
||||
/// An error may be returned if the data is not within the expected lengths.
|
||||
#[deprecated = "use Mnemonic::try_from_slice"]
|
||||
pub fn from_entropy(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
|
||||
MnemonicBase::try_from_slice(bytes)
|
||||
}
|
||||
|
||||
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data may be of a size
|
||||
/// of a factor of 4, up to 1024 bytes.
|
||||
///
|
||||
#[deprecated = "Use Mnemonic::from_array"]
|
||||
pub fn from_nonstandard_bytes<const N: usize>(bytes: [u8; N]) -> MnemonicBase<W> {
|
||||
MnemonicBase::from_array(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashSet, fs::File, io::Read};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn can_load_wordlist() {
|
||||
let _wordlist = English::get_singleton();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reproduces_its_own_seed() {
|
||||
let mut random_handle = File::open("/dev/random").unwrap();
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
random_handle.read_exact(&mut entropy[..]).unwrap();
|
||||
let mnemonic = super::Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
|
||||
let new_entropy = mnemonic.as_bytes();
|
||||
assert_eq!(new_entropy, entropy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conforms_to_trezor_tests() {
|
||||
let content = include_str!("data/vectors.json");
|
||||
let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap();
|
||||
|
||||
for test in jsonobj["english"].as_array().unwrap() {
|
||||
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else {
|
||||
panic!("bad test: {test}");
|
||||
};
|
||||
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
|
||||
|
||||
let mnemonic = Mnemonic::try_from_slice(&hex).unwrap();
|
||||
|
||||
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_bip39_crate() {
|
||||
let mut random_handle = File::open("/dev/random").unwrap();
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
random_handle.read_exact(&mut entropy[..]).unwrap();
|
||||
let my_mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
|
||||
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
|
||||
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
|
||||
assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed(""));
|
||||
assert_eq!(
|
||||
my_mnemonic.generate_seed("testing"),
|
||||
their_mnemonic.to_seed("testing")
|
||||
);
|
||||
assert_ne!(
|
||||
my_mnemonic.generate_seed("test1"),
|
||||
their_mnemonic.to_seed("test2")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_rate_of_duplicate_words() {
|
||||
let tests = 100_000;
|
||||
let mut count = 0.;
|
||||
let entropy = &mut [0u8; 256 / 8];
|
||||
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||
let mut hs = HashSet::<usize>::with_capacity(24);
|
||||
|
||||
for _ in 0..tests {
|
||||
random.read_exact(&mut entropy[..]).unwrap();
|
||||
let mnemonic = Mnemonic::try_from_slice(&entropy[..256 / 8]).unwrap();
|
||||
let words = mnemonic.words();
|
||||
hs.clear();
|
||||
hs.extend(words);
|
||||
if hs.len() != 24 {
|
||||
count += 1.;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Birthday problem math is: 0.126532
|
||||
// Set values to (about) 1 below, 1 above
|
||||
// Source: https://en.wikipedia.org/wiki/Birthday_problem
|
||||
let min = 11.5;
|
||||
let max = 13.5;
|
||||
assert!(
|
||||
count > f64::from(tests) * min / 100.,
|
||||
"{count} probability should be more than {min}%: {}",
|
||||
count / f64::from(tests)
|
||||
);
|
||||
assert!(
|
||||
count < f64::from(tests) * max / 100.,
|
||||
"{count} probability should be more than {max}%: {}",
|
||||
count / f64::from(tests)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_do_up_to_8192_bits() {
|
||||
let mut entropy = [0u8; 1024];
|
||||
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||
random.read_exact(&mut entropy[..]).unwrap();
|
||||
let mnemonic = Mnemonic::from_array(entropy);
|
||||
let words = mnemonic.words();
|
||||
assert_eq!(words.len(), 768);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn fails_over_8192_bits() {
|
||||
let entropy = &mut [0u8; 1024 + 4];
|
||||
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||
random.read_exact(&mut entropy[..]).unwrap();
|
||||
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn fails_over_invalid_size() {
|
||||
let entropy = &mut [0u8; 255];
|
||||
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||
random.read_exact(&mut entropy[..]).unwrap();
|
||||
let _mnemonic = Mnemonic::from_raw_bytes(&entropy[..]);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "keyfork-prompt"
|
||||
version = "0.1.2"
|
||||
version = "0.1.0"
|
||||
description = "Prompt management utilities for Keyfork"
|
||||
repository = "https://git.distrust.co/public/keyfork"
|
||||
edition = "2021"
|
||||
|
@ -10,10 +10,9 @@ license = "MIT"
|
|||
|
||||
[features]
|
||||
default = ["mnemonic"]
|
||||
mnemonic = ["keyfork-mnemonic"]
|
||||
mnemonic = ["keyfork-mnemonic-util"]
|
||||
|
||||
[dependencies]
|
||||
keyfork-bug = { workspace = true }
|
||||
keyfork-crossterm = { workspace = true, default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] }
|
||||
keyfork-mnemonic = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true }
|
||||
keyfork-crossterm = { version = "0.27.1", path = "../keyfork-crossterm", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] }
|
||||
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util", optional = true }
|
||||
thiserror = "1.0.51"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#![allow(missing_docs)]
|
||||
//!
|
||||
|
||||
use std::io::{stdin, stdout};
|
||||
|
||||
|
@ -7,8 +7,6 @@ use keyfork_prompt::{
|
|||
Terminal, PromptHandler,
|
||||
};
|
||||
|
||||
use keyfork_mnemonic::English;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut mgr = Terminal::new(stdin(), stdout())?;
|
||||
let transport_validator = mnemonic::MnemonicSetValidator {
|
||||
|
@ -18,21 +16,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
word_lengths: [24, 48],
|
||||
};
|
||||
|
||||
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
|
||||
let mnemonics = mgr.prompt_validated_wordlist(
|
||||
"Enter a 9-word and 24-word mnemonic: ",
|
||||
&Default::default(),
|
||||
3,
|
||||
transport_validator.to_fn(),
|
||||
)?;
|
||||
assert_eq!(mnemonics[0].as_bytes().len(), 12);
|
||||
assert_eq!(mnemonics[1].as_bytes().len(), 32);
|
||||
assert_eq!(mnemonics[0].entropy().len(), 12);
|
||||
assert_eq!(mnemonics[1].entropy().len(), 32);
|
||||
|
||||
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
|
||||
let mnemonics = mgr.prompt_validated_wordlist(
|
||||
"Enter a 24 and 48-word mnemonic: ",
|
||||
&Default::default(),
|
||||
3,
|
||||
combine_validator.to_fn(),
|
||||
)?;
|
||||
assert_eq!(mnemonics[0].as_bytes().len(), 32);
|
||||
assert_eq!(mnemonics[1].as_bytes().len(), 64);
|
||||
assert_eq!(mnemonics[0].entropy().len(), 32);
|
||||
assert_eq!(mnemonics[1].entropy().len(), 64);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use std::borrow::Borrow;
|
||||
|
||||
#[cfg(feature = "mnemonic")]
|
||||
use keyfork_mnemonic::Wordlist;
|
||||
use keyfork_mnemonic_util::Wordlist;
|
||||
|
||||
///
|
||||
pub mod terminal;
|
||||
|
@ -51,31 +51,31 @@ pub trait PromptHandler {
|
|||
/// could not be read.
|
||||
fn prompt_input(&mut self, prompt: &str) -> Result<String>;
|
||||
|
||||
/// Prompt the user for input based on a wordlist. A language must be specified as the generic
|
||||
/// parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
||||
/// Prompt the user for input based on a wordlist.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the message was not able to be displayed or if the input
|
||||
/// could not be read.
|
||||
#[cfg(feature = "mnemonic")]
|
||||
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> where X: Wordlist;
|
||||
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String>;
|
||||
|
||||
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
||||
/// provided parser function, returning the type from the parser. A language must be specified
|
||||
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
||||
/// provided parser function, returning the type from the parser.
|
||||
///
|
||||
/// # Errors
|
||||
/// The method may return an error if the message was not able to be displayed, if the input
|
||||
/// could not be read, or if the parser returned an error.
|
||||
#[cfg(feature = "mnemonic")]
|
||||
fn prompt_validated_wordlist<X, V>(
|
||||
fn prompt_validated_wordlist<V, F, E>(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
wordlist: &Wordlist,
|
||||
retries: u8,
|
||||
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
||||
validator_fn: F,
|
||||
) -> Result<V, Error>
|
||||
where
|
||||
X: Wordlist;
|
||||
F: Fn(String) -> Result<V, E>,
|
||||
E: std::error::Error;
|
||||
|
||||
/// Prompt the user for a passphrase, which is hidden while typing.
|
||||
///
|
||||
|
@ -90,12 +90,15 @@ pub trait PromptHandler {
|
|||
/// # Errors
|
||||
/// The method may return an error if the message was not able to be displayed, if the input
|
||||
/// could not be read, or if the parser returned an error.
|
||||
fn prompt_validated_passphrase<V>(
|
||||
fn prompt_validated_passphrase<V, F, E>(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
retries: u8,
|
||||
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
||||
) -> Result<V, Error>;
|
||||
validator_fn: F,
|
||||
) -> Result<V, Error>
|
||||
where
|
||||
F: Fn(String) -> Result<V, E>,
|
||||
E: std::error::Error;
|
||||
|
||||
/// Prompt the user with a [`Message`].
|
||||
///
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
//! A terminal prompt handler.
|
||||
//!
|
||||
//! This prompt handler uses a raw terminal device to read inputs and uses ANSI escape codes to
|
||||
//! provide formatting for prompts. Because of these reasons, it is not intended to be
|
||||
//! machine-readable.
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
|
||||
os::fd::AsRawFd,
|
||||
os::fd::AsRawFd, borrow::Borrow,
|
||||
};
|
||||
|
||||
use keyfork_crossterm::{
|
||||
|
@ -19,9 +12,7 @@ use keyfork_crossterm::{
|
|||
ExecutableCommand, QueueableCommand,
|
||||
};
|
||||
|
||||
use keyfork_bug::bug;
|
||||
|
||||
use crate::{Error, Message, PromptHandler, Wordlist};
|
||||
use crate::{PromptHandler, Message, Wordlist, Error};
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
@ -128,15 +119,9 @@ where
|
|||
W: Write + AsRawFd,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
self.write
|
||||
.execute(DisableBracketedPaste)
|
||||
.expect(bug!("can't restore bracketed paste"));
|
||||
self.write
|
||||
.execute(LeaveAlternateScreen)
|
||||
.expect(bug!("can't leave alternate screen"));
|
||||
self.terminal
|
||||
.disable_raw_mode()
|
||||
.expect(bug!("can't disable raw mode"));
|
||||
self.write.execute(DisableBracketedPaste).unwrap();
|
||||
self.write.execute(LeaveAlternateScreen).unwrap();
|
||||
self.terminal.disable_raw_mode().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,13 +155,11 @@ where
|
|||
fn lock(&mut self) -> TerminalGuard<'_, R, W> {
|
||||
TerminalGuard::new(&mut self.read, &mut self.write, &mut self.terminal)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<R, W> PromptHandler for Terminal<R, W>
|
||||
where
|
||||
R: Read + Sized,
|
||||
W: Write + AsRawFd + Sized,
|
||||
{
|
||||
impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + AsRawFd + Sized {
|
||||
|
||||
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
|
||||
let mut terminal = self.lock().alternate_screen()?;
|
||||
terminal
|
||||
|
@ -199,18 +182,20 @@ where
|
|||
}
|
||||
|
||||
#[cfg(feature = "mnemonic")]
|
||||
fn prompt_validated_wordlist<X, V>(
|
||||
fn prompt_validated_wordlist<V, F, E>(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
wordlist: &Wordlist,
|
||||
retries: u8,
|
||||
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
||||
validator_fn: F,
|
||||
) -> Result<V, Error>
|
||||
where
|
||||
X: Wordlist,
|
||||
F: Fn(String) -> Result<V, E>,
|
||||
E: std::error::Error,
|
||||
{
|
||||
let mut last_error = None;
|
||||
for _ in 0..retries {
|
||||
let s = self.prompt_wordlist::<X>(prompt)?;
|
||||
let s = self.prompt_wordlist(prompt, wordlist)?;
|
||||
match validator_fn(s) {
|
||||
Ok(v) => return Ok(v),
|
||||
Err(e) => {
|
||||
|
@ -229,13 +214,7 @@ where
|
|||
|
||||
#[cfg(feature = "mnemonic")]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String>
|
||||
where
|
||||
X: Wordlist,
|
||||
{
|
||||
let wordlist = X::get_singleton();
|
||||
let words = wordlist.to_str_array();
|
||||
|
||||
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> {
|
||||
let mut terminal = self
|
||||
.lock()
|
||||
.alternate_screen()?
|
||||
|
@ -300,20 +279,6 @@ where
|
|||
}
|
||||
KeyCode::Char(c) => {
|
||||
input.push(c);
|
||||
let entry_mode = std::env::var("KEYFORK_PROMPT_MNEMONIC_MODE");
|
||||
if entry_mode.is_ok_and(|mode| mode.to_ascii_lowercase() == "steel") {
|
||||
let word = input.split_whitespace().next_back().map(ToOwned::to_owned);
|
||||
if let Some(steel_word) = word {
|
||||
if steel_word.len() >= 4 {
|
||||
for word in words.iter().filter(|word| word.len() >= 4) {
|
||||
if word[..4] == steel_word {
|
||||
input.push_str(&word[4..]);
|
||||
input.push(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
|
@ -351,7 +316,7 @@ where
|
|||
let mut iter = printable_input.split_whitespace().peekable();
|
||||
|
||||
while let Some(word) = iter.next() {
|
||||
if words.contains(&word) {
|
||||
if wordlist.contains(word) {
|
||||
terminal.queue(PrintStyledContent(word.green()))?;
|
||||
} else {
|
||||
terminal.queue(PrintStyledContent(word.red()))?;
|
||||
|
@ -372,12 +337,16 @@ where
|
|||
Ok(input)
|
||||
}
|
||||
|
||||
fn prompt_validated_passphrase<V>(
|
||||
fn prompt_validated_passphrase<V, F, E>(
|
||||
&mut self,
|
||||
prompt: &str,
|
||||
retries: u8,
|
||||
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
||||
) -> Result<V, Error> {
|
||||
validator_fn: F,
|
||||
) -> Result<V, Error>
|
||||
where
|
||||
F: Fn(String) -> Result<V, E>,
|
||||
E: std::error::Error,
|
||||
{
|
||||
let mut last_error = None;
|
||||
for _ in 0..retries {
|
||||
let s = self.prompt_passphrase(prompt)?;
|
||||
|
|
|
@ -12,7 +12,7 @@ pub trait Validator {
|
|||
type Error;
|
||||
|
||||
/// Create a validator function from the given parameters.
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>>;
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>>;
|
||||
}
|
||||
|
||||
/// A PIN could not be validated from the given input.
|
||||
|
@ -29,84 +29,6 @@ pub enum PinError {
|
|||
/// The PIN contained invalid characters.
|
||||
#[error("PIN contained invalid characters (found {0} at position {1})")]
|
||||
InvalidCharacters(char, usize),
|
||||
|
||||
/// The provided PIN had either too many repeated characters or too many sequential characters.
|
||||
#[error("PIN contained too many repeated or sequential characters")]
|
||||
InsecurePIN,
|
||||
}
|
||||
|
||||
/// Validate that a PIN is of a certain length, matches a range of characters, and does not use
|
||||
/// incrementing or decrementing sequences of characters.
|
||||
///
|
||||
/// The validator determines a score for a passphrase and, if the score is high enough, returns an
|
||||
/// error.
|
||||
///
|
||||
/// Score is calculated based on:
|
||||
/// * how many sequential characters are in the passphrase (ascending or descending)
|
||||
/// * how many repeated characters are in the passphrase
|
||||
#[derive(Default, Clone)]
|
||||
pub struct SecurePinValidator {
|
||||
/// The minimum length of provided PINs.
|
||||
pub min_length: Option<usize>,
|
||||
|
||||
/// The maximum length of provided PINs.
|
||||
pub max_length: Option<usize>,
|
||||
|
||||
/// The characters allowed by the PIN parser.
|
||||
pub range: Option<RangeInclusive<char>>,
|
||||
|
||||
/// Whether repeated characters count against the PIN.
|
||||
pub ignore_repeated_characters: bool,
|
||||
|
||||
/// Whether sequential characters count against the PIN.
|
||||
pub ignore_sequential_characters: bool,
|
||||
}
|
||||
|
||||
impl Validator for SecurePinValidator {
|
||||
type Output = String;
|
||||
type Error = PinError;
|
||||
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
|
||||
let min_len = self.min_length.unwrap_or(usize::MIN);
|
||||
let max_len = self.max_length.unwrap_or(usize::MAX);
|
||||
let range = self.range.clone().unwrap_or('0'..='9');
|
||||
let ignore_repeated_characters = self.ignore_repeated_characters;
|
||||
let ignore_sequential_characters = self.ignore_sequential_characters;
|
||||
Box::new(move |mut s: String| {
|
||||
s.truncate(s.trim_end().len());
|
||||
let len = s.len();
|
||||
if len < min_len {
|
||||
return Err(Box::new(PinError::TooShort(len, min_len)));
|
||||
}
|
||||
if len > max_len {
|
||||
return Err(Box::new(PinError::TooLong(len, max_len)));
|
||||
}
|
||||
let mut last_char = 0;
|
||||
let mut score = 0;
|
||||
for (index, ch) in s.chars().enumerate() {
|
||||
if !range.contains(&ch) {
|
||||
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
|
||||
}
|
||||
if [-1, 1].contains(&(ch as i32 - last_char))
|
||||
&& !ignore_sequential_characters
|
||||
{
|
||||
score += 1;
|
||||
}
|
||||
last_char = ch as i32;
|
||||
}
|
||||
let mut chars = s.chars().collect::<Vec<_>>();
|
||||
chars.sort();
|
||||
chars.dedup();
|
||||
if !ignore_repeated_characters {
|
||||
// SAFETY: the amount of characters can't have _increased_ since deduping
|
||||
score += s.chars().count() - chars.len();
|
||||
}
|
||||
if score * 2 > s.chars().count() {
|
||||
return Err(Box::new(PinError::InsecurePIN))
|
||||
}
|
||||
Ok(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that a PIN is of a certain length and matches a range of characters.
|
||||
|
@ -126,7 +48,7 @@ impl Validator for PinValidator {
|
|||
type Output = String;
|
||||
type Error = PinError;
|
||||
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, PinError>> {
|
||||
let min_len = self.min_length.unwrap_or(usize::MIN);
|
||||
let max_len = self.max_length.unwrap_or(usize::MAX);
|
||||
let range = self.range.clone().unwrap_or('0'..='9');
|
||||
|
@ -134,14 +56,14 @@ impl Validator for PinValidator {
|
|||
s.truncate(s.trim_end().len());
|
||||
let len = s.len();
|
||||
if len < min_len {
|
||||
return Err(Box::new(PinError::TooShort(len, min_len)));
|
||||
return Err(PinError::TooShort(len, min_len));
|
||||
}
|
||||
if len > max_len {
|
||||
return Err(Box::new(PinError::TooLong(len, max_len)));
|
||||
return Err(PinError::TooLong(len, max_len));
|
||||
}
|
||||
for (index, ch) in s.chars().enumerate() {
|
||||
if !range.contains(&ch) {
|
||||
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
|
||||
return Err(PinError::InvalidCharacters(ch, index));
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
|
@ -157,8 +79,7 @@ pub mod mnemonic {
|
|||
|
||||
use super::Validator;
|
||||
|
||||
use keyfork_bug::bug;
|
||||
use keyfork_mnemonic::{Mnemonic, MnemonicFromStrError};
|
||||
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError};
|
||||
|
||||
/// A mnemonic could not be validated from the given input.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
@ -202,13 +123,13 @@ pub mod mnemonic {
|
|||
type Output = Mnemonic;
|
||||
type Error = MnemonicValidationError;
|
||||
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Box<dyn std::error::Error>>> {
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Self::Error>> {
|
||||
let word_length = self.word_length.clone();
|
||||
Box::new(move |s: String| match word_length.as_ref() {
|
||||
Some(wl) => {
|
||||
let count = s.split_whitespace().count();
|
||||
if !wl.matches(count) {
|
||||
return Err(Box::new(Self::Error::InvalidLength(count, wl.clone())));
|
||||
return Err(Self::Error::InvalidLength(count, wl.clone()));
|
||||
}
|
||||
let m = Mnemonic::from_str(&s)?;
|
||||
Ok(m)
|
||||
|
@ -244,7 +165,7 @@ pub mod mnemonic {
|
|||
type Output = Mnemonic;
|
||||
type Error = MnemonicChoiceValidationError;
|
||||
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>> {
|
||||
let word_lengths = self.word_lengths.clone();
|
||||
Box::new(move |s: String| {
|
||||
let count = s.split_whitespace().count();
|
||||
|
@ -254,10 +175,10 @@ pub mod mnemonic {
|
|||
return Ok(m);
|
||||
}
|
||||
}
|
||||
Err(Box::new(MnemonicChoiceValidationError::InvalidLength(
|
||||
Err(MnemonicChoiceValidationError::InvalidLength(
|
||||
count,
|
||||
word_lengths.to_vec(),
|
||||
)))
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -286,7 +207,7 @@ pub mod mnemonic {
|
|||
type Output = [Mnemonic; N];
|
||||
type Error = MnemonicSetValidationError;
|
||||
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
|
||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>> {
|
||||
let word_lengths = self.word_lengths;
|
||||
Box::new(move |s: String| {
|
||||
let mut counter: usize = 0;
|
||||
|
@ -298,17 +219,15 @@ pub mod mnemonic {
|
|||
.take(word_length)
|
||||
.collect::<Vec<_>>();
|
||||
if words.len() != word_length {
|
||||
return Err(Box::new(MnemonicSetValidationError::InvalidSetLength(
|
||||
return Err(MnemonicSetValidationError::InvalidSetLength(
|
||||
word_set,
|
||||
words.len(),
|
||||
word_length,
|
||||
)));
|
||||
));
|
||||
}
|
||||
let mnemonic = match Mnemonic::from_str(&words.join(" ")) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
return Err(Box::new(Self::Error::MnemonicFromStrError(word_set, e)))
|
||||
}
|
||||
Err(e) => return Err(Self::Error::MnemonicFromStrError(word_set, e)),
|
||||
};
|
||||
output.push(mnemonic);
|
||||
counter += word_length;
|
||||
|
@ -316,7 +235,7 @@ pub mod mnemonic {
|
|||
|
||||
Ok(output
|
||||
.try_into()
|
||||
.expect(bug!("vec with capacity of const N was not filled")))
|
||||
.expect("vec with capacity of const N was not filled"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,4 +5,4 @@ edition = "2021"
|
|||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
smex = { workspace = true }
|
||||
smex = { version = "0.1.0", path = "../smex" }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//! SLIP-0010 test data for use by derivation tests.
|
||||
//! Source: <https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vectors>
|
||||
//! Source: https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vectors
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
|
@ -28,15 +28,7 @@ impl std::fmt::Display for DecodeError {
|
|||
impl std::error::Error for DecodeError {}
|
||||
|
||||
/// Encode a given input as a hex string.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// let data = b"hello world!";
|
||||
/// let result = smex::encode(&data);
|
||||
/// assert_eq!(result, "68656c6c6f20776f726c6421");
|
||||
/// ```
|
||||
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
||||
let input = input.as_ref();
|
||||
pub fn encode(input: &[u8]) -> String {
|
||||
let mut s = String::new();
|
||||
for byte in input {
|
||||
write!(s, "{byte:02x}").unwrap();
|
||||
|
@ -58,26 +50,7 @@ fn val(c: u8) -> Result<u8, DecodeError> {
|
|||
/// # Errors
|
||||
/// The function may error if a non-hex character is encountered or if the character count is not
|
||||
/// evenly divisible by two.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// let data = b"hello world!";
|
||||
/// let encoded = smex::encode(&data);
|
||||
/// let decoded = smex::decode(&encoded).unwrap();
|
||||
/// assert_eq!(data.as_slice(), decoded.as_slice());
|
||||
/// ```
|
||||
///
|
||||
/// The function may return an error if the given input is not valid hex.
|
||||
///
|
||||
/// ```rust,should_panic
|
||||
/// let data = b"hello world!";
|
||||
/// let mut encoded = smex::encode(&data);
|
||||
/// encoded.push('G');
|
||||
/// let decoded = smex::decode(&encoded).unwrap();
|
||||
/// assert_eq!(data.as_slice(), decoded.as_slice());
|
||||
/// ```
|
||||
pub fn decode(input: impl AsRef<str>) -> Result<Vec<u8>, DecodeError> {
|
||||
let input = input.as_ref();
|
||||
pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
|
||||
let len = input.len();
|
||||
if len % 2 != 0 {
|
||||
return Err(DecodeError::InvalidCharacterCount(len));
|
||||
|
|
104
deny.toml
104
deny.toml
|
@ -11,9 +11,6 @@
|
|||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
|
@ -25,7 +22,7 @@
|
|||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#"x86_64-unknown-linux-musl",
|
||||
#{ triple = "x86_64-unknown-linux-musl" },
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
|
@ -49,9 +46,6 @@ no-default-features = false
|
|||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
|
@ -63,20 +57,38 @@ feature-depth = 1
|
|||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The path where the advisory database is cloned/fetched into
|
||||
db-path = "~/.cargo/advisory-db"
|
||||
# The url(s) of the advisory databases to use
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# The lint level for security vulnerabilities
|
||||
vulnerability = "deny"
|
||||
# The lint level for unmaintained crates
|
||||
unmaintained = "warn"
|
||||
# The lint level for crates that have been yanked from their source registry
|
||||
yanked = "warn"
|
||||
# The lint level for crates with security notices. Note that as of
|
||||
# 2019-12-17 there are no security notice advisories in
|
||||
# https://github.com/rustsec/advisory-db
|
||||
notice = "warn"
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
|
||||
{ id = "RUSTSEC-2023-0071", reason = "Not applicable, vulnerable path is not used" },
|
||||
# Not applicable, RSA is not used for crypto operations in the dep it's
|
||||
# used for, openpgp-card
|
||||
"RUSTSEC-2023-0071",
|
||||
]
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
# will still output a note when they are encountered.
|
||||
# * None - CVSS Score 0.0
|
||||
# * Low - CVSS Score 0.1 - 3.9
|
||||
# * Medium - CVSS Score 4.0 - 6.9
|
||||
# * High - CVSS Score 7.0 - 8.9
|
||||
# * Critical - CVSS Score 9.0 - 10.0
|
||||
#severity-threshold =
|
||||
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
|
@ -87,6 +99,8 @@ ignore = [
|
|||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# The lint level for crates which do not have a detectable license
|
||||
unlicensed = "deny"
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
|
@ -99,9 +113,30 @@ allow = [
|
|||
"Unicode-DFS-2016",
|
||||
"LGPL-2.0",
|
||||
"LGPL-3.0",
|
||||
"Unicode-3.0",
|
||||
#"Apache-2.0 WITH LLVM-exception",
|
||||
]
|
||||
|
||||
# List of explicitly disallowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
deny = [
|
||||
#"Nokia",
|
||||
]
|
||||
# Lint level for licenses considered copyleft
|
||||
copyleft = "warn"
|
||||
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
|
||||
# * both - The license will be approved if it is both OSI-approved *AND* FSF
|
||||
# * either - The license will be approved if it is either OSI-approved *OR* FSF
|
||||
# * osi - The license will be approved if it is OSI approved
|
||||
# * fsf - The license will be approved if it is FSF Free
|
||||
# * osi-only - The license will be approved if it is OSI-approved *AND NOT* FSF
|
||||
# * fsf-only - The license will be approved if it is FSF *AND NOT* OSI-approved
|
||||
# * neither - This predicate is ignored and the default lint level is used
|
||||
allow-osi-fsf-free = "neither"
|
||||
# Lint level used when no other predicates are matched
|
||||
# 1. License isn't in the allow or deny lists
|
||||
# 2. License isn't copyleft
|
||||
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
|
||||
default = "deny"
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
|
@ -112,7 +147,7 @@ confidence-threshold = 0.8
|
|||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||
{ allow = ["BSL-1.0"], name = "xxhash-rust", version = "*" },
|
||||
]
|
||||
|
||||
|
@ -120,8 +155,10 @@ exceptions = [
|
|||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
#crate = "ring"
|
||||
# The name of the crate the clarification applies to
|
||||
#name = "ring"
|
||||
# The optional version constraint for the crate
|
||||
#version = "*"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
|
@ -130,8 +167,8 @@ exceptions = [
|
|||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[licenses.private]
|
||||
|
@ -171,24 +208,25 @@ workspace-default-features = "allow"
|
|||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
{ name = "serde", version = ">1.0.171, <1.0.184", reason = "ships with prebuilt binaries" }
|
||||
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
|
||||
{ name = "serde", version = ">1.0.171, <1.0.184" }
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
#name = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
|
@ -209,18 +247,14 @@ deny = [
|
|||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
{ name = "windows-sys" },
|
||||
{ name = "windows-targets" },
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
{{#include links.md}}
|
||||
|
||||
## Dependencies
|
||||
|
||||
Keyfork has different dependencies depending on the feature set used for
|
||||
|
@ -68,3 +66,5 @@ cargo install --index https://git.distrust.co/public/_cargo-index keyfork-entrop
|
|||
# Confirmed to work as of 2024-01-17.
|
||||
cargo install --locked --path crates/util/keyfork-entropy --bin keyfork-entropy --features bin
|
||||
```
|
||||
|
||||
[SBOM]: https://en.wikipedia.org/wiki/SBOM
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
<!-- vim:set et sts=0 sw=2 ts=2: -->
|
||||
{{ #include links.md }}
|
||||
# Summary
|
||||
|
||||
# User Guide
|
||||
|
||||
- [Introduction to Keyfork](./introduction.md)
|
||||
- [Installing Keyfork](./INSTALL.md)
|
||||
- [Security Considerations](./security.md)
|
||||
- [Shard Commands](./shard.md)
|
||||
|
@ -33,4 +31,3 @@
|
|||
- [Provisioners](./dev-guide/provisioners.md)
|
||||
- [Auditing Dependencies](./dev-guide/auditing.md)
|
||||
- [Entropy Guide](./dev-guide/entropy.md)
|
||||
- [The Shard Protocol](./dev-guide/shard-protocol.md)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
{{#include ../links.md}}
|
||||
|
||||
# keyfork-derive-key
|
||||
|
||||
Derive a key from a given derivation path.
|
||||
|
@ -20,3 +18,5 @@ the shell silently ignoring the single quotes in the derivation path.
|
|||
|
||||
Hex-encoded private key. Note that this is not the _extended_ private key, and
|
||||
can't be used to derive further data.
|
||||
|
||||
[`keyforkd`]: ./bin/keyforkd.md
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue