Compare commits
23 Commits
keyfork-qr
...
main
Author | SHA1 | Date |
---|---|---|
|
64c75085f4 | |
|
00e35bcb7d | |
|
d0019a93f0 | |
|
020fa4d25e | |
|
76ca4b0812 | |
|
53665cac2e | |
|
a1c3d52c14 | |
|
674e2e93c5 | |
|
88a05f23ac | |
|
98b9dbb811 | |
|
723194fdd7 | |
|
db19b30bfe | |
|
0243212c80 | |
|
083eb16b39 | |
|
aa8526cda0 | |
|
e1c3e38fc7 | |
|
4e342ac7a9 | |
|
35e0eb57a0 | |
|
c232828290 | |
|
8756c3d233 | |
|
c95ed0b729 | |
|
19fbb51d12 | |
|
adb5293f1d |
162
CHANGELOG.md
162
CHANGELOG.md
|
@ -1,3 +1,165 @@
|
||||||
|
# Keyfork v0.3.2
|
||||||
|
|
||||||
|
This is another bugfix release, allowing the derivation of Shard keys.
|
||||||
|
|
||||||
|
|
||||||
|
### Changes in keyfork:
|
||||||
|
|
||||||
|
```
|
||||||
|
6ffcdc3 add derivation path for Shard keys
|
||||||
|
```
|
||||||
|
|
||||||
|
# Keyfork v0.3.1
|
||||||
|
|
||||||
|
This is a bugfix release, resolving an issue with Keyfork Shard not having a
|
||||||
|
exit condition for when a valid QR code was scanned.
|
||||||
|
|
||||||
|
### Changes in keyfork-shard:
|
||||||
|
|
||||||
|
```
|
||||||
|
d0019a9 keyfork-shard: break loop when receiving valid QR code
|
||||||
|
```
|
||||||
|
|
||||||
|
# Keyfork v0.3.0
|
||||||
|
|
||||||
|
The Wizard is Dead. Long Live the Mnemonic Generator.
|
||||||
|
|
||||||
|
The `keyfork wizard` subcommand was previously used to perform complex
|
||||||
|
operations that couldn't be performed with just `keyfork mnemonic generate`.
|
||||||
|
Since we've introduced complexity into `keyfork mnemonic generate`, it only
|
||||||
|
makes sense to consolidate all mnemonic generation complexity into one
|
||||||
|
location. Therefore, `keyfork mnemonic generate` should be a one-stop shop from
|
||||||
|
going to zero entropy to 256 bits of entropy. :)
|
||||||
|
|
||||||
|
The following operations are added:
|
||||||
|
|
||||||
|
* `keyfork mnemonic generate --derive=<derivation>`: Allow for the immediate
|
||||||
|
derivation of a key. The value passed will be parsed directly as though
|
||||||
|
`keyfork derive` were run. For example,
|
||||||
|
`keyfork mnemonic generate --derive='openpgp "Ryan Heywood"'` generates an
|
||||||
|
OpenPGP Transferable Secret Key that is nearly-identical to one generated by
|
||||||
|
`keyfork derive openpgp "Ryan Heywood"`, with the only exception being the
|
||||||
|
time the signatures were created.
|
||||||
|
* `keyfork mnemonic generate --encrypt-to <keyring>`: Encrypt the mnemonic to
|
||||||
|
an existing OpenPGP keyring or certificate.
|
||||||
|
* `keyfork mnemonic generate --shard-to <shardfile>`: Shard the mnemonic to
|
||||||
|
an existing Keyfork Shardfile.
|
||||||
|
* `keyfork mnemonic generate --shard <config>`: Shard the mnemonic to an
|
||||||
|
existing set of OpenPGP certificates.
|
||||||
|
* `keyfork mnemonic generate --encrypt-to-self <file>`: Encrypt the mnemonic to
|
||||||
|
an OpenPGP certificate generated in `--derive` or `--provision`
|
||||||
|
* `keyfork mnemonic generate --shard-to-self <file>,<config>`: Shard the
|
||||||
|
mnemonic to freshly generated certificates, provisioned to OpenPGP
|
||||||
|
smartcards. This option replaces the traditional Keyfork Wizard, which has
|
||||||
|
been removed.
|
||||||
|
* `keyfork mnemonic generate --provision`: Provision a key derived from the new
|
||||||
|
mnemonic, which can be used for `--encrypt-to-self`, or to just bypass
|
||||||
|
needing to load the mnemonic to provision with it.
|
||||||
|
|
||||||
|
Along with these changes, some other minor additions were added:
|
||||||
|
|
||||||
|
* QR code retries in the Shard mechanism are now implemented.
|
||||||
|
* `keyfork-qrcode` now prefers libzbar and can compile with both.
|
||||||
|
* `keyfork-prompt` should now work better on AirgapOS and Linux terminals.
|
||||||
|
|
||||||
|
### Changes in keyfork:
|
||||||
|
|
||||||
|
```
|
||||||
|
53665ca keyfork: the wizard is dead! long live the mnemonic generator!
|
||||||
|
a1c3d52 keyfork: restructure wizard shard key generation
|
||||||
|
674e2e9 keyfork: restructure CLI commands to act more like the other commands
|
||||||
|
723194f keyfork mnemonic generate: userid equivalency, rename provisioner cert_output to output
|
||||||
|
db19b30 keyfork mnemonic generate: feedback improvements
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyfork-bug:
|
||||||
|
|
||||||
|
Add `keyfork_bug::assert!()` for asserting with Keyfork Bug printing.
|
||||||
|
|
||||||
|
```
|
||||||
|
88a05f2 keyfork-prompt: add choice mechanism, & add to keyfork-shard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyfork-prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
88a05f2 keyfork-prompt: add choice mechanism, & add to keyfork-shard
|
||||||
|
0243212 keyfork-prompt: clear terminal before leaving alt screen; fixes linux terminal
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyfork-qrcode:
|
||||||
|
|
||||||
|
```
|
||||||
|
98b9dbb keyfork-qrcode: restructure to prefer libzbar and compile with both enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyfork-shard:
|
||||||
|
|
||||||
|
```
|
||||||
|
88a05f2 keyfork-prompt: add choice mechanism, & add to keyfork-shard
|
||||||
|
aa8526c Release keyfork-shard v0.3.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyfork-zbar:
|
||||||
|
|
||||||
|
```
|
||||||
|
98b9dbb keyfork-qrcode: restructure to prefer libzbar and compile with both enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyforkd:
|
||||||
|
|
||||||
|
```
|
||||||
|
674e2e9 keyfork: restructure CLI commands to act more like the other commands
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Keyfork v0.2.6
|
||||||
|
|
||||||
|
* The `--daemon` flag has been added for `keyfork recover` subcommands.
|
||||||
|
* `keyfork mnemonic generate` now has a bunch more options, to improve the out-of-the-box experience.
|
||||||
|
* `keyfork shard metadata` can be used to get the threshold and OpenPGP certificates.
|
||||||
|
* `keyfork derive openpgp` now correctly provides private keys, instead of public keys.
|
||||||
|
|
||||||
|
### Changes in keyfork:
|
||||||
|
|
||||||
|
```
|
||||||
|
4e342ac keyfork: add `--daemon`
|
||||||
|
c232828 superpower `keyfork mnemonic generate`
|
||||||
|
8756c3d keyfork wizard generate-shard-secret: allow exporting certificates and cross-sign generated keys
|
||||||
|
c95ed0b keyfork shard metadata: initial commit
|
||||||
|
adb5293 keyfork derive openpgp: export secret keys instead of public certs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyfork-derive-openpgp:
|
||||||
|
|
||||||
|
```
|
||||||
|
adb5293 keyfork derive openpgp: export secret keys instead of public certs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyfork-prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
35e0eb5 keyfork-prompt: use raw mode for input
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyfork-shard:
|
||||||
|
|
||||||
|
```
|
||||||
|
c95ed0b keyfork shard metadata: initial commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyfork-tests:
|
||||||
|
|
||||||
|
```
|
||||||
|
19fbb51 keyfork-tests: initial commit. also, fixup test_util's Panicable to not be generic. it's always unit type
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes in keyforkd:
|
||||||
|
|
||||||
|
```
|
||||||
|
19fbb51 keyfork-tests: initial commit. also, fixup test_util's Panicable to not be generic. it's always unit type
|
||||||
|
```
|
||||||
|
|
||||||
# Keyfork v0.2.5
|
# Keyfork v0.2.5
|
||||||
|
|
||||||
### Changes in keyfork:
|
### Changes in keyfork:
|
||||||
|
|
|
@ -147,6 +147,22 @@ dependencies = [
|
||||||
"term",
|
"term",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assert_cmd"
|
||||||
|
version = "2.0.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"bstr",
|
||||||
|
"doc-comment",
|
||||||
|
"libc",
|
||||||
|
"predicates",
|
||||||
|
"predicates-core",
|
||||||
|
"predicates-tree",
|
||||||
|
"wait-timeout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
|
@ -466,6 +482,17 @@ dependencies = [
|
||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"regex-automata 0.4.9",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "buffered-reader"
|
name = "buffered-reader"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
@ -602,6 +629,12 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.38"
|
version = "0.4.38"
|
||||||
|
@ -889,6 +922,12 @@ dependencies = [
|
||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "difflib"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
@ -933,6 +972,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doc-comment"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dsa"
|
name = "dsa"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
@ -1336,10 +1381,22 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi 0.13.3+wasi-0.2.2",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ghash"
|
name = "ghash"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -1740,7 +1797,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork"
|
name = "keyfork"
|
||||||
version = "0.2.5"
|
version = "0.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"card-backend-pcsc",
|
"card-backend-pcsc",
|
||||||
|
@ -1758,11 +1815,14 @@ dependencies = [
|
||||||
"keyforkd",
|
"keyforkd",
|
||||||
"keyforkd-client",
|
"keyforkd-client",
|
||||||
"keyforkd-models",
|
"keyforkd-models",
|
||||||
|
"nix",
|
||||||
"openpgp-card",
|
"openpgp-card",
|
||||||
"openpgp-card-sequoia",
|
"openpgp-card-sequoia",
|
||||||
"sequoia-openpgp",
|
"sequoia-openpgp",
|
||||||
"serde",
|
"serde",
|
||||||
|
"shlex",
|
||||||
"smex",
|
"smex",
|
||||||
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
@ -1776,7 +1836,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-bug"
|
name = "keyfork-bug"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-crossterm"
|
name = "keyfork-crossterm"
|
||||||
|
@ -1813,7 +1873,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-derive-openpgp"
|
name = "keyfork-derive-openpgp"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
@ -1884,7 +1944,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-prompt"
|
name = "keyfork-prompt"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"keyfork-bug",
|
"keyfork-bug",
|
||||||
"keyfork-crossterm",
|
"keyfork-crossterm",
|
||||||
|
@ -1894,8 +1954,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-qrcode"
|
name = "keyfork-qrcode"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
"image",
|
"image",
|
||||||
"keyfork-bug",
|
"keyfork-bug",
|
||||||
"keyfork-zbar",
|
"keyfork-zbar",
|
||||||
|
@ -1906,7 +1967,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-shard"
|
name = "keyfork-shard"
|
||||||
version = "0.3.0"
|
version = "0.3.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
@ -1936,9 +1997,18 @@ dependencies = [
|
||||||
"smex",
|
"smex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keyfork-tests"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"assert_cmd",
|
||||||
|
"keyforkd",
|
||||||
|
"sequoia-openpgp",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-zbar"
|
name = "keyfork-zbar"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"keyfork-zbar-sys",
|
"keyfork-zbar-sys",
|
||||||
|
@ -1956,7 +2026,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyforkd"
|
name = "keyforkd"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"hex-literal",
|
"hex-literal",
|
||||||
|
@ -2188,7 +2258,7 @@ dependencies = [
|
||||||
"hermit-abi 0.3.9",
|
"hermit-abi 0.3.9",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2198,7 +2268,7 @@ version = "7.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44e6ff4a94e5d34a1fd5abbd39418074646e2fa51b257198701330f22fcd6936"
|
checksum = "44e6ff4a94e5d34a1fd5abbd39418074646e2fa51b257198701330f22fcd6936"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"libc",
|
"libc",
|
||||||
"nettle-sys",
|
"nettle-sys",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
@ -2225,6 +2295,18 @@ version = "1.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
@ -2661,6 +2743,33 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates"
|
||||||
|
version = "3.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"difflib",
|
||||||
|
"predicates-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates-core"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates-tree"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
|
||||||
|
dependencies = [
|
||||||
|
"predicates-core",
|
||||||
|
"termtree",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.25"
|
version = "0.2.25"
|
||||||
|
@ -2725,7 +2834,7 @@ version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2743,7 +2852,7 @@ version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
@ -2813,9 +2922,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rqrr"
|
name = "rqrr"
|
||||||
version = "0.7.1"
|
version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad0cd0432e6beb2f86aa4c8af1bb5edcf3c9bcb9d4836facc048664205458575"
|
checksum = "f126a9b02152815d84315316e7a759ee18a216d057095d56d19cec68a428b385"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"g2p",
|
"g2p",
|
||||||
"image",
|
"image",
|
||||||
|
@ -2972,7 +3081,7 @@ dependencies = [
|
||||||
"ed25519",
|
"ed25519",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"flate2",
|
"flate2",
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"idea",
|
"idea",
|
||||||
"idna",
|
"idna",
|
||||||
"lalrpop",
|
"lalrpop",
|
||||||
|
@ -3260,12 +3369,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.14.0"
|
version = "3.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
"getrandom 0.3.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
|
@ -3292,6 +3402,12 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termtree"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
|
@ -3600,6 +3716,15 @@ version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wait-timeout"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
@ -3616,6 +3741,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.13.3+wasi-0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.95"
|
version = "0.2.95"
|
||||||
|
@ -3827,6 +3961,15 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rt"
|
||||||
|
version = "0.33.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "write16"
|
name = "write16"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
|
@ -23,6 +23,7 @@ members = [
|
||||||
"crates/util/keyfork-prompt",
|
"crates/util/keyfork-prompt",
|
||||||
"crates/util/keyfork-slip10-test-data",
|
"crates/util/keyfork-slip10-test-data",
|
||||||
"crates/util/smex",
|
"crates/util/smex",
|
||||||
|
"crates/tests",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
@ -75,6 +76,10 @@ thiserror = "1.0.56"
|
||||||
tokio = "1.35.1"
|
tokio = "1.35.1"
|
||||||
v4l = "0.14.0"
|
v4l = "0.14.0"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
|
tempfile = "3.17.1"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|
||||||
[profile.dev.package.keyfork-qrcode]
|
[profile.dev.package.keyfork-qrcode]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* Add and review a new blurb to the changelog by running the
|
* Add and review a new blurb to the changelog by running the
|
||||||
`make-changelog-blurb.sh` script and appending the result to the top of
|
`make-changelog-blurb.sh` script and appending the result to the top of
|
||||||
the file.
|
the file.
|
||||||
|
* Make sure to add some human-readable snippets at the top!
|
||||||
* Update all versions of crates listed in the changelog.
|
* Update all versions of crates listed in the changelog.
|
||||||
* Commit changes.
|
* Commit changes.
|
||||||
* Run the `sign-new-versions.sh` script to tag the new versions.
|
* Run the `sign-new-versions.sh` script to tag the new versions.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyforkd"
|
name = "keyforkd"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ tower = { version = "0.4.13", features = ["tokio", "util"] }
|
||||||
# Personally audited
|
# Personally audited
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
tempfile = { version = "3.10.0", default-features = false }
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex-literal = { workspace = true }
|
hex-literal = { workspace = true }
|
||||||
|
|
|
@ -26,7 +26,7 @@ pub enum UninstantiableError {}
|
||||||
/// };
|
/// };
|
||||||
/// assert!(closure().is_ok());
|
/// assert!(closure().is_ok());
|
||||||
/// ```
|
/// ```
|
||||||
pub type Panicable<T> = std::result::Result<T, UninstantiableError>;
|
pub type Panicable = std::result::Result<(), UninstantiableError>;
|
||||||
|
|
||||||
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
|
/// 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
|
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
|
||||||
|
@ -62,9 +62,9 @@ pub type Panicable<T> = std::result::Result<T, UninstantiableError>;
|
||||||
/// }).unwrap();
|
/// }).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(clippy::missing_errors_doc)]
|
#[allow(clippy::missing_errors_doc)]
|
||||||
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
|
pub fn run_test<F, E>(seed: &[u8], closure: F) -> std::result::Result<(), E>
|
||||||
where
|
where
|
||||||
F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
|
F: FnOnce(&std::path::Path) -> std::result::Result<(), E> + Send + 'static,
|
||||||
E: Send + 'static,
|
E: Send + 'static,
|
||||||
{
|
{
|
||||||
let rt = Builder::new_multi_thread()
|
let rt = Builder::new_multi_thread()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-derive-openpgp"
|
name = "keyfork-derive-openpgp"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
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_packets2() {
|
for packet in cert.as_tsk().into_packets() {
|
||||||
packet.serialize(&mut w)?;
|
packet.serialize(&mut w)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-shard"
|
name = "keyfork-shard"
|
||||||
version = "0.3.0"
|
version = "0.3.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
#![allow(clippy::expect_fun_call)]
|
#![allow(clippy::expect_fun_call)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{stdin, stdout, Read, Write},
|
io::{Read, Write},
|
||||||
sync::Mutex,
|
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
str::FromStr,
|
||||||
|
sync::{LazyLock, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
|
@ -22,7 +23,7 @@ use keyfork_prompt::{
|
||||||
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
|
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
|
||||||
Validator,
|
Validator,
|
||||||
},
|
},
|
||||||
Message as PromptMessage, PromptHandler, Terminal,
|
Message as PromptMessage, PromptHandler,
|
||||||
};
|
};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
@ -34,6 +35,30 @@ const PLAINTEXT_LENGTH: u8 = 32 // shard
|
||||||
+ 1; // length;
|
+ 1; // length;
|
||||||
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
|
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
|
enum RetryScanMnemonic {
|
||||||
|
Retry,
|
||||||
|
Continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl keyfork_prompt::Choice for RetryScanMnemonic {
|
||||||
|
fn identifier(&self) -> Option<char> {
|
||||||
|
Some(match self {
|
||||||
|
RetryScanMnemonic::Retry => 'r',
|
||||||
|
RetryScanMnemonic::Continue => 'c',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RetryScanMnemonic {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
RetryScanMnemonic::Retry => write!(f, "Retry scanning mnemonic."),
|
||||||
|
RetryScanMnemonic::Continue => write!(f, "Continue to manual mnemonic entry."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openpgp")]
|
#[cfg(feature = "openpgp")]
|
||||||
pub mod openpgp;
|
pub mod openpgp;
|
||||||
|
|
||||||
|
@ -140,7 +165,7 @@ pub trait Format {
|
||||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||||
) -> Result<(Vec<Share>, u8), Self::Error>;
|
) -> Result<(Vec<Share>, u8), Self::Error>;
|
||||||
|
|
||||||
/// Decrypt a single share and associated metadata from a reaable input. For the current
|
/// Decrypt a single share and associated metadata from a readable input. For the current
|
||||||
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
|
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
|
||||||
/// combine secrets.
|
/// combine secrets.
|
||||||
///
|
///
|
||||||
|
@ -154,6 +179,40 @@ pub trait Format {
|
||||||
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||||
) -> Result<(Share, u8), Self::Error>;
|
) -> Result<(Share, u8), Self::Error>;
|
||||||
|
|
||||||
|
/// Decrypt the public keys and metadata from encrypted data.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
|
||||||
|
/// could neither be encrypted nor parsed.
|
||||||
|
fn decrypt_metadata(
|
||||||
|
&self,
|
||||||
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
|
encrypted_data: &[Self::EncryptedData],
|
||||||
|
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||||
|
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error>;
|
||||||
|
|
||||||
|
/// Decrypt the public keys and metadata from a Shardfile.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if hte shardfile couldn't be read from or if the metadata
|
||||||
|
/// could neither be encrypted nor parsed.
|
||||||
|
fn decrypt_metadata_from_file(
|
||||||
|
&self,
|
||||||
|
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||||
|
reader: impl Read + Send + Sync,
|
||||||
|
prompt: Box<dyn PromptHandler>,
|
||||||
|
) -> Result<(u8, Vec<Self::PublicKey>), Self::Error> {
|
||||||
|
let private_keys = private_key_discovery
|
||||||
|
.map(|p| p.discover_private_keys())
|
||||||
|
.transpose()?;
|
||||||
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||||
|
self.decrypt_metadata(
|
||||||
|
private_keys,
|
||||||
|
&encrypted_messages,
|
||||||
|
Rc::new(Mutex::new(prompt)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Decrypt multiple shares and combine them to recreate a secret.
|
/// Decrypt multiple shares and combine them to recreate a secret.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
@ -213,19 +272,28 @@ pub trait Format {
|
||||||
.lock()
|
.lock()
|
||||||
.expect(bug!(POISONED_MUTEX))
|
.expect(bug!(POISONED_MUTEX))
|
||||||
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
if let Ok(Some(qrcode_content)) =
|
loop {
|
||||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
if let Ok(Some(qrcode_content)) = keyfork_qrcode::scan_camera(
|
||||||
{
|
std::time::Duration::from_secs(*QRCODE_TIMEOUT),
|
||||||
let decoded_data = BASE64_STANDARD
|
0,
|
||||||
.decode(qrcode_content)
|
) {
|
||||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
let decoded_data = BASE64_STANDARD
|
||||||
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
|
.decode(qrcode_content)
|
||||||
} else {
|
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||||
prompt
|
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?);
|
||||||
.lock()
|
break;
|
||||||
.expect(bug!(POISONED_MUTEX))
|
} else {
|
||||||
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
|
||||||
};
|
let choice = keyfork_prompt::prompt_choice(
|
||||||
|
&mut **prompt,
|
||||||
|
"A QR code could not be scanned. Retry or continue?",
|
||||||
|
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
|
||||||
|
)?;
|
||||||
|
if choice == RetryScanMnemonic::Continue {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
||||||
|
@ -425,9 +493,13 @@ pub(crate) const HUNK_VERSION: u8 = 2;
|
||||||
pub(crate) const HUNK_OFFSET: usize = 2;
|
pub(crate) const HUNK_OFFSET: usize = 2;
|
||||||
|
|
||||||
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
||||||
const QRCODE_TIMEOUT: u64 = 60; // One minute
|
|
||||||
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
|
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
|
||||||
const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry.";
|
static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
|
||||||
|
std::env::var("KEYFORK_QRCODE_TIMEOUT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| u64::from_str(&t).ok())
|
||||||
|
.unwrap_or(60)
|
||||||
|
});
|
||||||
|
|
||||||
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
|
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
|
||||||
/// shares, and combine them.
|
/// shares, and combine them.
|
||||||
|
@ -442,7 +514,7 @@ const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry
|
||||||
/// The function may panic if it is given payloads generated using a version of Keyfork that is
|
/// The function may panic if it is given payloads generated using a version of Keyfork that is
|
||||||
/// incompatible with the currently running version.
|
/// incompatible with the currently running version.
|
||||||
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut pm = Terminal::new(stdin(), stdout())?;
|
let mut pm = keyfork_prompt::default_handler()?;
|
||||||
|
|
||||||
let mut iter_count = None;
|
let mut iter_count = None;
|
||||||
let mut shares = vec![];
|
let mut shares = vec![];
|
||||||
|
@ -489,23 +561,35 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
if let Ok(Some(qrcode_content)) =
|
loop {
|
||||||
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
|
if let Ok(Some(qrcode_content)) = keyfork_qrcode::scan_camera(
|
||||||
{
|
std::time::Duration::from_secs(*QRCODE_TIMEOUT),
|
||||||
let decoded_data = BASE64_STANDARD
|
0,
|
||||||
.decode(qrcode_content)
|
) {
|
||||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
let decoded_data = BASE64_STANDARD
|
||||||
assert_eq!(
|
.decode(qrcode_content)
|
||||||
decoded_data.len(),
|
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||||
// Include length of public key
|
assert_eq!(
|
||||||
ENCRYPTED_LENGTH as usize + 32,
|
decoded_data.len(),
|
||||||
bug!("invalid payload data")
|
// Include length of public key
|
||||||
);
|
ENCRYPTED_LENGTH as usize + 32,
|
||||||
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
bug!("invalid payload data")
|
||||||
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
);
|
||||||
} else {
|
let _ =
|
||||||
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
||||||
};
|
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
let choice = keyfork_prompt::prompt_choice(
|
||||||
|
&mut *pm,
|
||||||
|
"A QR code could not be scanned. Retry or continue?",
|
||||||
|
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
|
||||||
|
)?;
|
||||||
|
if choice == RetryScanMnemonic::Continue {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (pubkey, payload) = match (pubkey_data, payload_data) {
|
let (pubkey, payload) = match (pubkey_data, payload_data) {
|
||||||
|
@ -516,7 +600,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
};
|
};
|
||||||
|
|
||||||
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
|
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
|
||||||
&mut pm,
|
&mut *pm,
|
||||||
QRCODE_COULDNT_READ,
|
QRCODE_COULDNT_READ,
|
||||||
3,
|
3,
|
||||||
&*validator.to_fn(),
|
&*validator.to_fn(),
|
||||||
|
|
|
@ -549,6 +549,26 @@ impl Format for OpenPGP {
|
||||||
|
|
||||||
panic!("unable to decrypt shard");
|
panic!("unable to decrypt shard");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decrypt_metadata(
|
||||||
|
&self,
|
||||||
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
|
encrypted_data: &[Self::EncryptedData],
|
||||||
|
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
|
||||||
|
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error> {
|
||||||
|
let policy = NullPolicy::new();
|
||||||
|
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
|
||||||
|
let mut manager = SmartcardManager::new(prompt.clone())?;
|
||||||
|
let mut encrypted_messages = encrypted_data.iter();
|
||||||
|
|
||||||
|
let metadata = encrypted_messages
|
||||||
|
.next()
|
||||||
|
.expect(bug!(METADATA_MESSAGE_MISSING));
|
||||||
|
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
|
||||||
|
|
||||||
|
let (threshold, _root_cert, certs) = decode_metadata_v1(&metadata_content)?;
|
||||||
|
Ok((threshold, certs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyDiscovery<OpenPGP> for &Path {
|
impl KeyDiscovery<OpenPGP> for &Path {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork"
|
name = "keyfork"
|
||||||
version = "0.2.5"
|
version = "0.3.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ license = "AGPL-3.0-only"
|
||||||
default = [
|
default = [
|
||||||
"completion",
|
"completion",
|
||||||
"qrcode-decode-backend-rqrr",
|
"qrcode-decode-backend-rqrr",
|
||||||
"sequoia-crypto-backend-nettle",
|
"sequoia-crypto-backend-nettle",
|
||||||
]
|
]
|
||||||
|
|
||||||
completion = ["dep:clap_complete"]
|
completion = ["dep:clap_complete"]
|
||||||
|
@ -47,3 +47,6 @@ clap_complete = { version = "4.4.6", optional = true }
|
||||||
sequoia-openpgp = { workspace = true }
|
sequoia-openpgp = { workspace = true }
|
||||||
keyforkd-models.workspace = true
|
keyforkd-models.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
nix = { version = "0.29.0", default-features = false, features = ["process"] }
|
||||||
|
shlex = "1.3.0"
|
||||||
|
tempfile.workspace = true
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
//! Extensions to clap.
|
||||||
|
|
||||||
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
|
/// An error that occurred while parsing a base value or its
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ValueParseError {
|
||||||
|
/// No value was given; the required type could not be parsed.
|
||||||
|
#[error("No value was given")]
|
||||||
|
NoValue,
|
||||||
|
|
||||||
|
/// The first value could not properly be parsed.
|
||||||
|
#[error("Could not parse first value: {0}")]
|
||||||
|
BadParse(String),
|
||||||
|
|
||||||
|
/// Additional values were added, but not in a key=value format.
|
||||||
|
#[error("A key-value pair was not given")]
|
||||||
|
BadKeyValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper struct to parse key-value arguments, without any prior argument.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Options {
|
||||||
|
/// The values provided.
|
||||||
|
pub values: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Options {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut iter = self.values.iter().peekable();
|
||||||
|
while let Some((key, value)) = iter.next() {
|
||||||
|
write!(f, "{key}={value}")?;
|
||||||
|
if iter.peek().is_some() {
|
||||||
|
write!(f, ",")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Options {
|
||||||
|
type Err = ValueParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s.is_empty() {
|
||||||
|
return Ok(Default::default())
|
||||||
|
}
|
||||||
|
let values = s
|
||||||
|
.split(',')
|
||||||
|
.map(|value| {
|
||||||
|
let [k, v] = value
|
||||||
|
.splitn(2, '=')
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| ValueParseError::BadKeyValue)?;
|
||||||
|
Ok((k.to_string(), v.to_string()))
|
||||||
|
})
|
||||||
|
.collect::<Result<HashMap<String, String>, ValueParseError>>()?;
|
||||||
|
Ok(Self { values })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper struct for clap arguments that can contain additional arguments. For example:
|
||||||
|
/// `keyfork mnemonic generate --encrypt-to cert.asc,output=encrypted.asc`.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ValueWithOptions<T: FromStr>
|
||||||
|
where
|
||||||
|
T::Err: std::error::Error,
|
||||||
|
{
|
||||||
|
/// A mapping between keys and values.
|
||||||
|
pub values: HashMap<String, String>,
|
||||||
|
|
||||||
|
/// The first variable for the argument, such as a [`PathBuf`].
|
||||||
|
pub inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: std::str::FromStr> FromStr for ValueWithOptions<T>
|
||||||
|
where
|
||||||
|
<T as FromStr>::Err: std::error::Error,
|
||||||
|
{
|
||||||
|
type Err = ValueParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut values = s.split(',');
|
||||||
|
let first = values.next().ok_or(ValueParseError::NoValue)?;
|
||||||
|
let mut others = HashMap::new();
|
||||||
|
for value in values {
|
||||||
|
let [lhs, rhs] = value
|
||||||
|
.splitn(2, '=')
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| ValueParseError::BadKeyValue)?;
|
||||||
|
others.insert(lhs.to_string(), rhs.to_string());
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
inner: first
|
||||||
|
.parse()
|
||||||
|
.map_err(|e: <T as FromStr>::Err| ValueParseError::BadParse(e.to_string()))?,
|
||||||
|
values: others,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +1,36 @@
|
||||||
use super::Keyfork;
|
use super::{Keyfork, create};
|
||||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||||
|
use std::{fmt::Display, io::Write, path::PathBuf};
|
||||||
|
|
||||||
use keyfork_derive_openpgp::{
|
use keyfork_derive_openpgp::openpgp::{
|
||||||
openpgp::{
|
armor::{Kind, Writer},
|
||||||
armor::{Kind, Writer},
|
packet::UserID,
|
||||||
packet::UserID,
|
serialize::Marshal,
|
||||||
serialize::Marshal,
|
types::KeyFlags,
|
||||||
types::KeyFlags,
|
Cert,
|
||||||
},
|
|
||||||
XPrvKey,
|
|
||||||
};
|
};
|
||||||
use keyfork_derive_path_data::paths;
|
use keyfork_derive_path_data::paths;
|
||||||
use keyfork_derive_util::{
|
use keyfork_derive_util::{
|
||||||
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
|
request::DerivationAlgorithm, DerivationIndex, DerivationPath, ExtendedPrivateKey as XPrv,
|
||||||
DerivationIndex, DerivationPath, IndexError,
|
IndexError, PrivateKey,
|
||||||
};
|
};
|
||||||
use keyforkd_client::Client;
|
use keyforkd_client::Client;
|
||||||
use keyforkd_models::Request;
|
|
||||||
|
type OptWrite = Option<Box<dyn Write>>;
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
pub trait Deriver {
|
||||||
|
type Prv: PrivateKey + Clone;
|
||||||
|
const DERIVATION_ALGORITHM: DerivationAlgorithm;
|
||||||
|
|
||||||
|
fn derivation_path(&self) -> DerivationPath;
|
||||||
|
|
||||||
|
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()>;
|
||||||
|
|
||||||
|
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
pub enum DeriveSubcommands {
|
pub enum DeriveSubcommands {
|
||||||
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
|
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
|
||||||
|
@ -34,14 +45,56 @@ pub enum DeriveSubcommands {
|
||||||
#[command(name = "openpgp")]
|
#[command(name = "openpgp")]
|
||||||
OpenPGP(OpenPGP),
|
OpenPGP(OpenPGP),
|
||||||
|
|
||||||
/// Derive a bare key for a specific algorithm, in a given format.
|
/// Derive an Ed25519 key for a specific algorithm, in a given format.
|
||||||
Key(Key),
|
Key(Key),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derivation path to use when deriving OpenPGP keys.
|
||||||
|
#[derive(ValueEnum, Clone, Debug, Default)]
|
||||||
|
pub enum Path {
|
||||||
|
/// The default derivation path; no additional index is used.
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
|
||||||
|
/// The Disaster Recovery index.
|
||||||
|
DisasterRecovery,
|
||||||
|
|
||||||
|
/// The Shard index.
|
||||||
|
Shard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Path {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Path {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Path::Default => "default",
|
||||||
|
Path::DisasterRecovery => "disaster-recovery",
|
||||||
|
Path::Shard => "shard",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derivation_path(&self) -> DerivationPath {
|
||||||
|
match self {
|
||||||
|
Self::Default => paths::OPENPGP.clone(),
|
||||||
|
Self::DisasterRecovery => paths::OPENPGP_DISASTER_RECOVERY.clone(),
|
||||||
|
Self::Shard => paths::OPENPGP_SHARD.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args, Clone, Debug)]
|
#[derive(Args, Clone, Debug)]
|
||||||
pub struct OpenPGP {
|
pub struct OpenPGP {
|
||||||
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
/// Default User ID for the certificate, using the OpenPGP User ID format.
|
||||||
user_id: String,
|
user_id: String,
|
||||||
|
|
||||||
|
/// Derivation path to use when deriving OpenPGP keys.
|
||||||
|
#[arg(long, required = false, default_value = "default")]
|
||||||
|
derivation_path: Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A format for exporting a key.
|
/// A format for exporting a key.
|
||||||
|
@ -83,6 +136,18 @@ impl std::str::FromStr for Slug {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Slug {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match (self.0.inner() & (0b1 << 31)).to_be_bytes().as_slice() {
|
||||||
|
[0, 0, 0, 0] => Ok(()),
|
||||||
|
[0, 0, 0, bytes @ ..] | [0, 0, bytes @ ..] | [0, bytes @ ..] | [bytes @ ..] => f
|
||||||
|
.write_str(
|
||||||
|
std::str::from_utf8(&bytes[..]).expect("slug constructed from non-utf8"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args, Clone, Debug)]
|
#[derive(Args, Clone, Debug)]
|
||||||
pub struct Key {
|
pub struct Key {
|
||||||
/// The derivation algorithm to derive a key for.
|
/// The derivation algorithm to derive a key for.
|
||||||
|
@ -98,18 +163,34 @@ pub struct Key {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeriveSubcommands {
|
impl DeriveSubcommands {
|
||||||
fn handle(&self, account: DerivationIndex) -> Result<()> {
|
fn handle(&self, account: DerivationIndex, is_public: bool, writer: OptWrite) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
DeriveSubcommands::OpenPGP(opgp) => opgp.handle(account),
|
DeriveSubcommands::OpenPGP(opgp) => {
|
||||||
DeriveSubcommands::Key(key) => key.handle(account),
|
let path = opgp.derivation_path();
|
||||||
|
let xprv = Client::discover_socket()?
|
||||||
|
.request_xprv::<<OpenPGP as Deriver>::Prv>(&path.chain_push(account))?;
|
||||||
|
if is_public {
|
||||||
|
opgp.derive_public_with_xprv(writer, xprv)
|
||||||
|
} else {
|
||||||
|
opgp.derive_with_xprv(writer, xprv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeriveSubcommands::Key(key) => {
|
||||||
|
let path = key.derivation_path();
|
||||||
|
let xprv = Client::discover_socket()?
|
||||||
|
.request_xprv::<<Key as Deriver>::Prv>(&path.chain_push(account))?;
|
||||||
|
if is_public {
|
||||||
|
key.derive_public_with_xprv(writer, xprv)
|
||||||
|
} else {
|
||||||
|
key.derive_with_xprv(writer, xprv)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenPGP {
|
impl OpenPGP {
|
||||||
pub fn handle(&self, account: DerivationIndex) -> Result<()> {
|
fn cert_from_xprv(&self, xprv: keyfork_derive_openpgp::XPrv) -> Result<Cert> {
|
||||||
let path = paths::OPENPGP.clone().chain_push(account);
|
|
||||||
// TODO: should this be customizable?
|
|
||||||
let subkeys = vec![
|
let subkeys = vec![
|
||||||
KeyFlags::empty().set_certification(),
|
KeyFlags::empty().set_certification(),
|
||||||
KeyFlags::empty().set_signing(),
|
KeyFlags::empty().set_signing(),
|
||||||
|
@ -118,40 +199,100 @@ impl OpenPGP {
|
||||||
.set_storage_encryption(),
|
.set_storage_encryption(),
|
||||||
KeyFlags::empty().set_authentication(),
|
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 userid = UserID::from(&*self.user_id);
|
||||||
|
keyfork_derive_openpgp::derive(xprv, &subkeys, &userid).map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for packet in cert.into_packets2() {
|
impl Deriver for OpenPGP {
|
||||||
packet.serialize(&mut w)?;
|
type Prv = keyfork_derive_openpgp::XPrvKey;
|
||||||
|
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
|
||||||
|
|
||||||
|
fn derivation_path(&self) -> DerivationPath {
|
||||||
|
self.derivation_path.derivation_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
|
||||||
|
let cert = self.cert_from_xprv(xprv)?;
|
||||||
|
let writer = match writer {
|
||||||
|
Some(w) => w,
|
||||||
|
None => {
|
||||||
|
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
|
||||||
|
let file = create(&path)?;
|
||||||
|
Box::new(file)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut writer = Writer::new(writer, Kind::SecretKey)?;
|
||||||
|
for packet in cert.as_tsk().into_packets() {
|
||||||
|
packet.serialize(&mut writer)?;
|
||||||
}
|
}
|
||||||
|
writer.finalize()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
w.finalize()?;
|
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
|
||||||
|
let cert = self.cert_from_xprv(xprv)?;
|
||||||
|
let writer = match writer {
|
||||||
|
Some(w) => w,
|
||||||
|
None => {
|
||||||
|
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
|
||||||
|
let file = create(&path)?;
|
||||||
|
Box::new(file)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut writer = Writer::new(writer, Kind::PublicKey)?;
|
||||||
|
for packet in cert.into_packets2() {
|
||||||
|
packet.serialize(&mut writer)?;
|
||||||
|
}
|
||||||
|
writer.finalize()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Key {
|
impl Deriver for Key {
|
||||||
pub fn handle(&self, account: DerivationIndex) -> Result<()> {
|
// HACK: We're abusing that we use the same key as OpenPGP. Maybe we should use ed25519_dalek.
|
||||||
let mut client = keyforkd_client::Client::discover_socket()?;
|
type Prv = keyfork_derive_openpgp::XPrvKey;
|
||||||
let path = DerivationPath::default()
|
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
|
||||||
.chain_push(self.slug.0.clone())
|
|
||||||
.chain_push(account);
|
|
||||||
let request = DerivationRequest::new(self.derivation_algorithm.clone(), &path);
|
|
||||||
let request = Request::Derivation(request);
|
|
||||||
let derived_key: DerivationResponse = client.request(&request)?.try_into()?;
|
|
||||||
|
|
||||||
let formatted = match self.format {
|
fn derivation_path(&self) -> DerivationPath {
|
||||||
KeyFormat::Hex => smex::encode(derived_key.data),
|
DerivationPath::default().chain_push(self.slug.0.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
|
||||||
|
let (formatted, ext) = match self.format {
|
||||||
|
KeyFormat::Hex => (smex::encode(xprv.private_key().to_bytes()), "hex"),
|
||||||
KeyFormat::Base64 => {
|
KeyFormat::Base64 => {
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
BASE64_STANDARD.encode(derived_key.data)
|
(BASE64_STANDARD.encode(xprv.private_key().to_bytes()), "b64")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let filename =
|
||||||
|
PathBuf::from(smex::encode(xprv.public_key().to_bytes())).with_extension(ext);
|
||||||
|
if let Some(mut writer) = writer {
|
||||||
|
writeln!(writer, "{formatted}")?;
|
||||||
|
} else {
|
||||||
|
std::fs::write(&filename, formatted)?;
|
||||||
|
}
|
||||||
|
|
||||||
eprintln!("{formatted}");
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
|
||||||
|
let (formatted, ext) = match self.format {
|
||||||
|
KeyFormat::Hex => (smex::encode(xprv.public_key().to_bytes()), "hex"),
|
||||||
|
KeyFormat::Base64 => {
|
||||||
|
use base64::prelude::*;
|
||||||
|
(BASE64_STANDARD.encode(xprv.public_key().to_bytes()), "b64")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let filename =
|
||||||
|
PathBuf::from(smex::encode(xprv.public_key().to_bytes())).with_extension(ext);
|
||||||
|
if let Some(mut writer) = writer {
|
||||||
|
writeln!(writer, "{formatted}")?;
|
||||||
|
} else {
|
||||||
|
std::fs::write(&filename, formatted)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,7 +300,7 @@ impl Key {
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
pub struct Derive {
|
pub struct Derive {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: DeriveSubcommands,
|
pub(crate) command: DeriveSubcommands,
|
||||||
|
|
||||||
/// Account ID. Required for all derivations.
|
/// Account ID. Required for all derivations.
|
||||||
///
|
///
|
||||||
|
@ -167,12 +308,45 @@ pub struct Derive {
|
||||||
/// account ID can often come as a hindrance in the future. As such, it is always required. If
|
/// 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`.
|
/// the account ID is not relevant, it is assumed to be `0`.
|
||||||
#[arg(long, global = true, default_value = "0")]
|
#[arg(long, global = true, default_value = "0")]
|
||||||
account_id: u32,
|
pub(crate) account_id: u32,
|
||||||
|
|
||||||
|
/// Whether derivation should return the public key or a private key.
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub(crate) public: bool,
|
||||||
|
|
||||||
|
/// Whether the file should be written to standard output, or to a filename generated by the
|
||||||
|
/// derivation system.
|
||||||
|
#[arg(long, global = true, default_value = "false")]
|
||||||
|
pub to_stdout: bool,
|
||||||
|
|
||||||
|
/// The file to write the derived public key to, if not standard output. If omitted, a filename
|
||||||
|
/// will be generated by the relevant deriver.
|
||||||
|
#[arg(long, global = true, conflicts_with = "to_stdout")]
|
||||||
|
pub output: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Derive {
|
impl Derive {
|
||||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||||
let account = DerivationIndex::new(self.account_id, true)?;
|
let account = DerivationIndex::new(self.account_id, true)?;
|
||||||
self.command.handle(account)
|
let writer = if let Some(output) = self.output.as_deref() {
|
||||||
|
Some(Box::new(std::fs::File::create(output)?) as Box<dyn Write>)
|
||||||
|
} else if self.to_stdout {
|
||||||
|
Some(Box::new(std::io::stdout()) as Box<dyn Write>)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
self.command.handle(account, self.public, writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for Derive {
|
||||||
|
type Err = clap::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
Derive::try_parse_from(
|
||||||
|
[String::from("derive")]
|
||||||
|
.into_iter()
|
||||||
|
.chain(shlex::Shlex::new(s)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,43 @@
|
||||||
use super::Keyfork;
|
use super::{
|
||||||
|
create,
|
||||||
|
derive::{self, Deriver},
|
||||||
|
provision,
|
||||||
|
Keyfork,
|
||||||
|
};
|
||||||
|
use crate::{clap_ext::*, config, openpgp_card::factory_reset_current_card};
|
||||||
|
use card_backend_pcsc::PcscBackend;
|
||||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||||
use std::fmt::Display;
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fmt::Display,
|
||||||
|
fs::File,
|
||||||
|
io::{IsTerminal, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use keyfork_derive_openpgp::{
|
||||||
|
openpgp::{
|
||||||
|
self,
|
||||||
|
armor::{Kind, Writer},
|
||||||
|
packet::{UserID, signature::SignatureBuilder},
|
||||||
|
policy::StandardPolicy,
|
||||||
|
serialize::{
|
||||||
|
stream::{Encryptor2, LiteralWriter, Message, Recipient},
|
||||||
|
Serialize,
|
||||||
|
},
|
||||||
|
types::{KeyFlags, SignatureType},
|
||||||
|
},
|
||||||
|
XPrv,
|
||||||
|
};
|
||||||
|
use keyfork_derive_util::DerivationIndex;
|
||||||
|
use keyfork_prompt::{
|
||||||
|
default_handler, prompt_validated_passphrase,
|
||||||
|
validators::{SecurePinValidator, Validator},
|
||||||
|
};
|
||||||
|
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||||
|
|
||||||
|
type StringMap = HashMap<String, String>;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub enum SeedSize {
|
pub enum SeedSize {
|
||||||
|
@ -59,6 +96,7 @@ impl From<&SeedSize> for usize {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, thiserror::Error)]
|
#[derive(Clone, Debug, thiserror::Error)]
|
||||||
pub enum MnemonicSeedSourceParseError {
|
pub enum MnemonicSeedSourceParseError {
|
||||||
#[error("Expected one of system, playing, tarot, dice")]
|
#[error("Expected one of system, playing, tarot, dice")]
|
||||||
|
@ -96,24 +134,41 @@ impl std::str::FromStr for MnemonicSeedSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MnemonicSeedSource {
|
impl MnemonicSeedSource {
|
||||||
pub fn handle(&self, size: &SeedSize) -> Result<String, Box<dyn std::error::Error>> {
|
pub fn handle(
|
||||||
|
&self,
|
||||||
|
size: &SeedSize,
|
||||||
|
) -> Result<keyfork_mnemonic::Mnemonic, Box<dyn std::error::Error>> {
|
||||||
let size = match size {
|
let size = match size {
|
||||||
SeedSize::Bits128 => 128,
|
SeedSize::Bits128 => 128,
|
||||||
SeedSize::Bits256 => 256,
|
SeedSize::Bits256 => 256,
|
||||||
};
|
};
|
||||||
let seed = match self {
|
let seed = match self {
|
||||||
MnemonicSeedSource::System => {
|
MnemonicSeedSource::System => keyfork_entropy::generate_entropy_of_size(size / 8)?,
|
||||||
keyfork_entropy::generate_entropy_of_size(size / 8)?
|
|
||||||
}
|
|
||||||
MnemonicSeedSource::Playing => todo!(),
|
MnemonicSeedSource::Playing => todo!(),
|
||||||
MnemonicSeedSource::Tarot => todo!(),
|
MnemonicSeedSource::Tarot => todo!(),
|
||||||
MnemonicSeedSource::Dice => todo!(),
|
MnemonicSeedSource::Dice => todo!(),
|
||||||
};
|
};
|
||||||
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?;
|
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?;
|
||||||
Ok(mnemonic.to_string())
|
Ok(mnemonic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An error occurred while performing an operation.
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// An error occurred when interacting iwth a file.
|
||||||
|
#[error("Error while performing IO operation on: {1}")]
|
||||||
|
IOContext(#[source] std::io::Error, PathBuf),
|
||||||
|
|
||||||
|
/// A required option was not provided.
|
||||||
|
#[error("The required option {0} was not provided")]
|
||||||
|
MissingOption(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context_stub<'a>(path: &'a Path) -> impl Fn(std::io::Error) -> Error + 'a {
|
||||||
|
|e| Error::IOContext(e, path.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
pub enum MnemonicSubcommands {
|
pub enum MnemonicSubcommands {
|
||||||
/// Generate a mnemonic using a given entropy source.
|
/// Generate a mnemonic using a given entropy source.
|
||||||
|
@ -124,6 +179,10 @@ pub enum MnemonicSubcommands {
|
||||||
/// method of generating a seed using system entropy, as well as various forms of loading
|
/// method of generating a seed using system entropy, as well as various forms of loading
|
||||||
/// physicalized entropy into a mnemonic. The mnemonic should be stored in a safe location
|
/// physicalized entropy into a mnemonic. The mnemonic should be stored in a safe location
|
||||||
/// (such as a Trezor "recovery seed card") and never persisted digitally.
|
/// (such as a Trezor "recovery seed card") and never persisted digitally.
|
||||||
|
///
|
||||||
|
/// When using the `--shard`, `--shard-to`, `--encrypt-to`, and `--encrypt-to-self` +
|
||||||
|
/// `--provision` arguments, the mnemonic is _not_ sent to output. The data for the mnemonic is
|
||||||
|
/// then either split using Keyfork Shard or encrypted using OpenPGP.
|
||||||
Generate {
|
Generate {
|
||||||
/// The source from where a seed is created.
|
/// The source from where a seed is created.
|
||||||
#[arg(long, value_enum, default_value_t = Default::default())]
|
#[arg(long, value_enum, default_value_t = Default::default())]
|
||||||
|
@ -132,17 +191,757 @@ pub enum MnemonicSubcommands {
|
||||||
/// The size of the mnemonic, in bits.
|
/// The size of the mnemonic, in bits.
|
||||||
#[arg(long, default_value_t = Default::default())]
|
#[arg(long, default_value_t = Default::default())]
|
||||||
size: SeedSize,
|
size: SeedSize,
|
||||||
|
|
||||||
|
/// Derive a key. By default, a private key is derived. Unlike other arguments in this
|
||||||
|
/// file, arguments must be passed using the format similar to the CLI. For example:
|
||||||
|
/// `--derive='openpgp --public "Ryan Heywood <ryan@distrust.co>"'` would be synonymous
|
||||||
|
/// with starting the Keyfork daemon with the provided mnemonic, then running
|
||||||
|
/// `keyfork derive openpgp --public "Ryan Heywood <ryan@distrust.co>"`.
|
||||||
|
///
|
||||||
|
/// The output of the derived key is written to a filename based on the content of the key;
|
||||||
|
/// for instance, OpenPGP keys are written to a file identifiable by the certificate's
|
||||||
|
/// fingerprint. This behavior can be changed by using the `--to-stdout` or `--output`
|
||||||
|
/// modifiers to the `--derive` command.
|
||||||
|
#[arg(long)]
|
||||||
|
derive: Option<derive::Derive>,
|
||||||
|
|
||||||
|
/// Encrypt the mnemonic to an OpenPGP certificate in the provided path.
|
||||||
|
///
|
||||||
|
/// When given arguments in the format `--encrypt-to input.asc,output=output.asc`, the
|
||||||
|
/// output of the encryption will be written to `output.asc`. Otherwise, the default
|
||||||
|
/// behavior is to write the output to `input.enc.asc`. If the output file already exists,
|
||||||
|
/// it will not be overwritten, and the command will exit unsuccessfully.
|
||||||
|
#[arg(long)]
|
||||||
|
encrypt_to: Option<Vec<ValueWithOptions<PathBuf>>>,
|
||||||
|
|
||||||
|
/// Shard the mnemonic to the certificates in the given Shardfile. Requires a decrypt
|
||||||
|
/// operation on the Shardfile to access the metadata and certificates.
|
||||||
|
///
|
||||||
|
/// When given arguments in the format `--shard-to input.asc,output=output.asc`, the
|
||||||
|
/// output of the encryption will be written to `output.asc`. Otherwise, the default
|
||||||
|
/// behavior is to write the output to `input.new.asc`. If the output file already exists,
|
||||||
|
/// it will not be overwritten, and the command will exit unsuccessfully.
|
||||||
|
#[arg(long)]
|
||||||
|
shard_to: Option<Vec<ValueWithOptions<PathBuf>>>,
|
||||||
|
|
||||||
|
/// Shard the mnemonic to the provided certificates.
|
||||||
|
///
|
||||||
|
/// The following additional arguments are available:
|
||||||
|
///
|
||||||
|
/// * threshold, m: the minimum amount of shares required to reconstitute the shard. By
|
||||||
|
/// default, this is the amount of certificates provided.
|
||||||
|
///
|
||||||
|
/// * max, n: the maximum amount of shares. When provided, this is used to ensure the
|
||||||
|
/// certificate count is correct. This is required when using `threshold` or `m`.
|
||||||
|
///
|
||||||
|
/// * output: the file to write the generated Shardfile to. By default, assuming the
|
||||||
|
/// certificate input is `input.asc`, the generated Shardfile would be written to
|
||||||
|
/// `input.shard.asc`.
|
||||||
|
#[arg(long)]
|
||||||
|
shard: Option<Vec<ValueWithOptions<PathBuf>>>,
|
||||||
|
|
||||||
|
/// Encrypt the mnemonic to an OpenPGP certificate derived from the mnemonic, writing the
|
||||||
|
/// output to the provided path. This command must be run in combination with
|
||||||
|
/// `--provision openpgp-card`, `--derive openpgp`, or another OpenPGP key derivation
|
||||||
|
/// mechanism, to ensure the generated mnemonic would be decryptable.
|
||||||
|
///
|
||||||
|
/// When used in combination with `--derive` or `--provision` with OpenPGP configurations,
|
||||||
|
/// the default behavior is to encrypt the mnemonic to all derived and provisioned
|
||||||
|
/// accounts. By default, the account `0` is used.
|
||||||
|
#[arg(long)]
|
||||||
|
encrypt_to_self: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Shard the mnemonic to freshly-generated OpenPGP certificates derived from the mnemonic,
|
||||||
|
/// writing the output to the provided path, and provisioning OpenPGP smartcards with the
|
||||||
|
/// new certificates.
|
||||||
|
///
|
||||||
|
/// The following additional arguments are required:
|
||||||
|
///
|
||||||
|
/// * threshold, m: the minimum amount of shares required to reconstitute the shard.
|
||||||
|
///
|
||||||
|
/// * max, n: the maximum amount of shares.
|
||||||
|
///
|
||||||
|
/// * cards_per_shard: the amount of OpenPGP smartcards to provision per shardholder.
|
||||||
|
///
|
||||||
|
/// * cert_output: the file to write all generated OpenPGP certificates to; if not
|
||||||
|
/// provided, files will be automatically generated for each certificate.
|
||||||
|
#[arg(long)]
|
||||||
|
shard_to_self: Option<ValueWithOptions<PathBuf>>,
|
||||||
|
|
||||||
|
/// Provision a key derived from the mnemonic to a piece of hardware such as an OpenPGP
|
||||||
|
/// smartcard. This argument is required when used with `--encrypt-to-self`.
|
||||||
|
///
|
||||||
|
/// Provisioners may choose to output a public key to the current directory by default, but
|
||||||
|
/// this functionality may be altered on a by-provisioner basis by providing the `output=`
|
||||||
|
/// option to `--provisioner-config`. Additionally, Keyfork may choose to disable
|
||||||
|
/// provisioner output if a matching public key has been derived using `--derive`, which
|
||||||
|
/// may allow for controlling additional metadata that is not relevant to the provisioned
|
||||||
|
/// keys, such as an OpenPGP User ID.
|
||||||
|
#[arg(long)]
|
||||||
|
provision: Option<provision::Provision>,
|
||||||
|
|
||||||
|
/// The amount of times the provisioner should be run. If provisioning multiple devices at
|
||||||
|
/// once, this number should be specified to the number of devices, and all devices should
|
||||||
|
/// be plugged into the system at the same time.
|
||||||
|
#[arg(long, requires = "provision", default_value = "1")]
|
||||||
|
provision_count: usize,
|
||||||
|
|
||||||
|
/// The configuration to pass to the provisioner. These values are specific to each
|
||||||
|
/// provisioner, and should be provided in a `key=value,key=value` format. Most
|
||||||
|
/// provisioners only expect an `output=` option, to be used in place of the default output
|
||||||
|
/// path, if the provisioner needs to write data to a file, such as an OpenPGP certificate.
|
||||||
|
#[arg(long, requires = "provision", default_value_t = Options::default())]
|
||||||
|
provision_config: Options,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: This function defaults to `.asc` in the event no extension is found.
|
||||||
|
// This is specific to OpenPGP. If you want to use this function elsewhere (why?),
|
||||||
|
// be sure to use a relevant extension for your context.
|
||||||
|
fn determine_valid_output_path<T: AsRef<Path>>(
|
||||||
|
path: &Path,
|
||||||
|
mid_ext: &str,
|
||||||
|
optional_path: Option<T>,
|
||||||
|
) -> PathBuf {
|
||||||
|
match optional_path {
|
||||||
|
Some(p) => p.as_ref().to_path_buf(),
|
||||||
|
None => {
|
||||||
|
let extension = match path.extension() {
|
||||||
|
Some(ext) => format!("{mid_ext}.{ext}", ext = ext.to_string_lossy()),
|
||||||
|
None => format!("{mid_ext}.asc"),
|
||||||
|
};
|
||||||
|
path.with_extension(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_extension_armored(path: &Path) -> bool {
|
||||||
|
match path.extension().and_then(|s| s.to_str()) {
|
||||||
|
Some("pgp") | Some("gpg") => false,
|
||||||
|
Some("asc") => true,
|
||||||
|
_ => {
|
||||||
|
eprintln!("unable to determine whether to armor file: {path:?}");
|
||||||
|
eprintln!("use .gpg, .pgp, or .asc extension, or `armor=true`");
|
||||||
|
eprintln!("defaulting to armored");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_encrypt_to(
|
||||||
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
|
path: &Path,
|
||||||
|
options: &StringMap,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let policy = StandardPolicy::new();
|
||||||
|
|
||||||
|
let output_file = determine_valid_output_path(path, "enc", options.get("output"));
|
||||||
|
|
||||||
|
let is_armored =
|
||||||
|
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
|
||||||
|
|
||||||
|
let certs = OpenPGP::discover_certs(path)?;
|
||||||
|
let valid_certs = certs
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.with_policy(&policy, None))
|
||||||
|
.collect::<openpgp::Result<Vec<_>>>()?;
|
||||||
|
let recipients = valid_certs.iter().flat_map(|valid_cert| {
|
||||||
|
let keys = valid_cert.keys().alive().for_storage_encryption();
|
||||||
|
keys.map(|key| Recipient::new(key.keyid(), key.key()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut output = vec![];
|
||||||
|
let message = Message::new(&mut output);
|
||||||
|
let encrypted_message = Encryptor2::for_recipients(message, recipients).build()?;
|
||||||
|
let mut literal_message = LiteralWriter::new(encrypted_message).build()?;
|
||||||
|
literal_message.write_all(mnemonic.to_string().as_bytes())?;
|
||||||
|
literal_message.write_all(b"\n")?;
|
||||||
|
literal_message.finalize()?;
|
||||||
|
|
||||||
|
let mut file = File::create(&output_file).map_err(context_stub(&output_file))?;
|
||||||
|
if is_armored {
|
||||||
|
let mut writer = Writer::new(file, Kind::Message)?;
|
||||||
|
writer.write_all(&output)?;
|
||||||
|
writer.finalize()?;
|
||||||
|
} else {
|
||||||
|
file.write_all(&output)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_encrypt_to_self(
|
||||||
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
|
path: &Path,
|
||||||
|
accounts: &[keyfork_derive_util::DerivationIndex],
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut certs = vec![];
|
||||||
|
|
||||||
|
for account in accounts.iter().cloned() {
|
||||||
|
let userid = UserID::from("Keyfork Temporary Key");
|
||||||
|
|
||||||
|
let subkeys = [
|
||||||
|
KeyFlags::empty().set_certification(),
|
||||||
|
KeyFlags::empty().set_signing(),
|
||||||
|
KeyFlags::empty()
|
||||||
|
.set_transport_encryption()
|
||||||
|
.set_storage_encryption(),
|
||||||
|
KeyFlags::empty().set_authentication(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let seed = mnemonic.generate_seed(None);
|
||||||
|
let xprv = XPrv::new(seed)?;
|
||||||
|
let derivation_path = keyfork_derive_path_data::paths::OPENPGP
|
||||||
|
.clone()
|
||||||
|
.chain_push(account);
|
||||||
|
|
||||||
|
let cert =
|
||||||
|
keyfork_derive_openpgp::derive(xprv.derive_path(&derivation_path)?, &subkeys, &userid)?;
|
||||||
|
|
||||||
|
certs.push(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = tempfile::NamedTempFile::new()?;
|
||||||
|
|
||||||
|
let mut writer = Writer::new(&mut file, Kind::PublicKey)?;
|
||||||
|
for cert in certs {
|
||||||
|
cert.serialize(&mut writer)?;
|
||||||
|
}
|
||||||
|
writer.finalize()?;
|
||||||
|
|
||||||
|
let temp_path = file.into_temp_path();
|
||||||
|
|
||||||
|
// a sneaky bit of DRY
|
||||||
|
do_encrypt_to(
|
||||||
|
mnemonic,
|
||||||
|
&temp_path,
|
||||||
|
&StringMap::from([(String::from("output"), path.to_string_lossy().to_string())]),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
temp_path.close()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
#[error("Either the threshold(m) or the max(n) values are missing")]
|
||||||
|
struct MissingThresholdOrMax;
|
||||||
|
|
||||||
|
fn do_shard(
|
||||||
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
|
path: &Path,
|
||||||
|
options: &StringMap,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let output_file = determine_valid_output_path(path, "shard", options.get("output"));
|
||||||
|
|
||||||
|
let is_armored =
|
||||||
|
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
|
||||||
|
|
||||||
|
let threshold = options
|
||||||
|
.get("threshold")
|
||||||
|
.or_else(|| options.get("m"))
|
||||||
|
.map(|s| u8::from_str(s))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let max = options
|
||||||
|
.get("max")
|
||||||
|
.or_else(|| options.get("n"))
|
||||||
|
.map(|s| u8::from_str(s))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let certs = OpenPGP::discover_certs(path)?;
|
||||||
|
|
||||||
|
// if neither are set: false
|
||||||
|
// if both are set: false
|
||||||
|
// if only one is set: true
|
||||||
|
|
||||||
|
if threshold.is_some() ^ max.is_some() {
|
||||||
|
return Err(MissingThresholdOrMax)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (threshold, max) = match threshold.zip(max) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
let len = u8::try_from(certs.len())?;
|
||||||
|
(len, len)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
|
|
||||||
|
let mut output = vec![];
|
||||||
|
openpgp.shard_and_encrypt(threshold, max, mnemonic.as_bytes(), &certs[..], &mut output)?;
|
||||||
|
|
||||||
|
let mut file = File::create(&output_file).map_err(context_stub(&output_file))?;
|
||||||
|
if is_armored {
|
||||||
|
file.write_all(&output)?;
|
||||||
|
} else {
|
||||||
|
todo!("keyfork does not handle binary shardfiles");
|
||||||
|
/*
|
||||||
|
* NOTE: this code works, but can't be recombined by Keyfork.
|
||||||
|
* therefore, we'll error, before someone tries to use it.
|
||||||
|
let mut dearmor = Reader::from_bytes(&output, ReaderMode::Tolerant(None));
|
||||||
|
std::io::copy(&mut dearmor, &mut file)?;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_shard_to(
|
||||||
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
|
path: &Path,
|
||||||
|
options: &StringMap,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let output_file = determine_valid_output_path(path, "new", options.get("output"));
|
||||||
|
|
||||||
|
let is_armored =
|
||||||
|
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
|
||||||
|
|
||||||
|
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
|
let prompt = default_handler()?;
|
||||||
|
|
||||||
|
let input = File::open(path)?;
|
||||||
|
let (threshold, certs) = openpgp.decrypt_metadata_from_file(
|
||||||
|
Some(&[][..]), // the things i must do to avoid qualifying types.
|
||||||
|
input,
|
||||||
|
prompt,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut output = vec![];
|
||||||
|
openpgp.shard_and_encrypt(
|
||||||
|
threshold,
|
||||||
|
u8::try_from(certs.len())?,
|
||||||
|
mnemonic.as_bytes(),
|
||||||
|
&certs[..],
|
||||||
|
&mut output,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut file = File::create(&output_file).map_err(context_stub(&output_file))?;
|
||||||
|
if is_armored {
|
||||||
|
file.write_all(&output)?;
|
||||||
|
} else {
|
||||||
|
todo!("keyfork does not handle binary shardfiles");
|
||||||
|
/*
|
||||||
|
* NOTE: this code works, but can't be recombined by Keyfork.
|
||||||
|
* therefore, we'll error, before someone tries to use it.
|
||||||
|
let mut dearmor = Reader::from_bytes(&output, ReaderMode::Tolerant(None));
|
||||||
|
std::io::copy(&mut dearmor, &mut file)?;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_key(seed: [u8; 64], index: u8) -> Result<openpgp::Cert, Box<dyn std::error::Error>> {
|
||||||
|
let subkeys = vec![
|
||||||
|
KeyFlags::empty().set_certification(),
|
||||||
|
KeyFlags::empty().set_signing(),
|
||||||
|
KeyFlags::empty()
|
||||||
|
.set_transport_encryption()
|
||||||
|
.set_storage_encryption(),
|
||||||
|
KeyFlags::empty().set_authentication(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let subkey = DerivationIndex::new(u32::from(index), true)?;
|
||||||
|
let path = keyfork_derive_path_data::paths::OPENPGP_SHARD.clone().chain_push(subkey);
|
||||||
|
let xprv = XPrv::new(seed)
|
||||||
|
.expect("could not construct master key from seed")
|
||||||
|
.derive_path(&path)?;
|
||||||
|
let userid = UserID::from(format!("Keyfork Shard {index}"));
|
||||||
|
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
|
||||||
|
Ok(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cross_sign_certs(certs: &mut [openpgp::Cert]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let policy = StandardPolicy::new();
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_to_owned)]
|
||||||
|
for signing_cert in certs.to_vec() {
|
||||||
|
let mut certify_key = signing_cert
|
||||||
|
.with_policy(&policy, None)?
|
||||||
|
.keys()
|
||||||
|
.unencrypted_secret()
|
||||||
|
.for_certification()
|
||||||
|
.next()
|
||||||
|
.expect("certify key unusable/not found")
|
||||||
|
.key()
|
||||||
|
.clone()
|
||||||
|
.into_keypair()?;
|
||||||
|
for signable_cert in certs.iter_mut() {
|
||||||
|
let sb = SignatureBuilder::new(SignatureType::GenericCertification);
|
||||||
|
let userid = signable_cert
|
||||||
|
.userids()
|
||||||
|
.next()
|
||||||
|
.expect("a signable user ID is necessary to create web of trust");
|
||||||
|
let signature = sb.sign_userid_binding(
|
||||||
|
&mut certify_key,
|
||||||
|
signable_cert.primary_key().key(),
|
||||||
|
&userid,
|
||||||
|
)?;
|
||||||
|
let changed;
|
||||||
|
(*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?;
|
||||||
|
assert!(
|
||||||
|
changed,
|
||||||
|
"OpenPGP certificate was unchanged after inserting packets"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn do_shard_to_self(
|
||||||
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
|
path: &Path,
|
||||||
|
options: &StringMap,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let seed = mnemonic.generate_seed(None);
|
||||||
|
let mut pm = default_handler()?;
|
||||||
|
let mut certs = vec![];
|
||||||
|
let mut seen_cards = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
let threshold: u8 = options
|
||||||
|
.get("threshold")
|
||||||
|
.or(options.get("m"))
|
||||||
|
.ok_or(Error::MissingOption("threshold"))?
|
||||||
|
.parse()?;
|
||||||
|
let max: u8 = options
|
||||||
|
.get("max")
|
||||||
|
.or(options.get("n"))
|
||||||
|
.ok_or(Error::MissingOption("max"))?
|
||||||
|
.parse()?;
|
||||||
|
let cards_per_shard = options
|
||||||
|
.get("cards_per_shard")
|
||||||
|
.as_deref()
|
||||||
|
.map(|cps| u8::from_str(cps))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let pin_validator = SecurePinValidator {
|
||||||
|
min_length: Some(8),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.to_fn();
|
||||||
|
|
||||||
|
for index in 0..max {
|
||||||
|
let cert = derive_key(seed, index)?;
|
||||||
|
for i in 0..cards_per_shard.unwrap_or(1) {
|
||||||
|
pm.prompt_message(keyfork_prompt::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(keyfork_prompt::Message::Text(
|
||||||
|
"No smart card was found. Please plug in a smart card and press enter"
|
||||||
|
.to_string(),
|
||||||
|
))?;
|
||||||
|
};
|
||||||
|
let pin = prompt_validated_passphrase(
|
||||||
|
&mut *pm,
|
||||||
|
"Please enter the new smartcard PIN: ",
|
||||||
|
3,
|
||||||
|
&pin_validator,
|
||||||
|
)?;
|
||||||
|
factory_reset_current_card(
|
||||||
|
&mut |application_identifier| {
|
||||||
|
if seen_cards.contains(&application_identifier) {
|
||||||
|
// we were given a previously-seen card, error
|
||||||
|
// we're gonna panic because this is a significant error
|
||||||
|
panic!("Previously used card {application_identifier} was reused");
|
||||||
|
} else {
|
||||||
|
seen_cards.insert(application_identifier);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pin.trim(),
|
||||||
|
pin.trim(),
|
||||||
|
&cert,
|
||||||
|
&openpgp::policy::NullPolicy::new(),
|
||||||
|
card_backend,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
certs.push(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
cross_sign_certs(&mut certs)?;
|
||||||
|
|
||||||
|
let opgp = OpenPGP;
|
||||||
|
let output = File::create(path)?;
|
||||||
|
opgp.shard_and_encrypt(
|
||||||
|
threshold,
|
||||||
|
certs.len() as u8,
|
||||||
|
mnemonic.as_bytes(),
|
||||||
|
&certs[..],
|
||||||
|
output,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
match options.get("cert_output") {
|
||||||
|
Some(path) => {
|
||||||
|
let cert_file = std::fs::File::create(path)?;
|
||||||
|
let mut writer = Writer::new(cert_file, Kind::PublicKey)?;
|
||||||
|
for cert in &certs {
|
||||||
|
cert.serialize(&mut writer)?;
|
||||||
|
}
|
||||||
|
writer.finalize()?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
for cert in &certs {
|
||||||
|
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
|
||||||
|
let file = create(&path)?;
|
||||||
|
let mut writer = Writer::new(file, Kind::PublicKey)?;
|
||||||
|
cert.serialize(&mut writer)?;
|
||||||
|
writer.finalize()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_provision(
|
||||||
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
|
provision: &provision::Provision,
|
||||||
|
count: usize,
|
||||||
|
config: &HashMap<String, String>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
assert!(
|
||||||
|
provision.subcommand.is_none(),
|
||||||
|
"provisioner was given a subcommand; this functionality is not supported"
|
||||||
|
);
|
||||||
|
|
||||||
|
let identifiers = match &provision.identifier {
|
||||||
|
Some(identifier) => {
|
||||||
|
vec![identifier.clone()]
|
||||||
|
}
|
||||||
|
None => provision
|
||||||
|
.provisioner_name
|
||||||
|
.discover()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, _ctx)| name)
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
identifiers.len(),
|
||||||
|
count,
|
||||||
|
"amount of provisionable devices discovered did not match provisioner count"
|
||||||
|
);
|
||||||
|
|
||||||
|
for identifier in identifiers {
|
||||||
|
let provisioner_with_identifier = provision::Provision {
|
||||||
|
identifier: Some(identifier),
|
||||||
|
..provision.clone()
|
||||||
|
};
|
||||||
|
let mut provisioner = config::Provisioner::try_from(provisioner_with_identifier)?;
|
||||||
|
match &mut provisioner.metadata {
|
||||||
|
Some(metadata) => {
|
||||||
|
metadata.extend(config.clone().into_iter());
|
||||||
|
}
|
||||||
|
metadata @ None => {
|
||||||
|
*metadata = Some(config.clone());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
provision
|
||||||
|
.provisioner_name
|
||||||
|
.provision_with_mnemonic(mnemonic, provisioner)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_derive(
|
||||||
|
mnemonic: &keyfork_mnemonic::MnemonicBase<keyfork_mnemonic::English>,
|
||||||
|
deriver: &derive::Derive,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let writer = if let Some(output) = deriver.output.as_deref() {
|
||||||
|
Some(Box::new(std::fs::File::create(output)?) as Box<dyn Write>)
|
||||||
|
} else if deriver.to_stdout {
|
||||||
|
Some(Box::new(std::io::stdout()) as Box<dyn Write>)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
match deriver {
|
||||||
|
derive::Derive {
|
||||||
|
command: derive::DeriveSubcommands::OpenPGP(opgp),
|
||||||
|
account_id,
|
||||||
|
public,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
use keyfork_derive_openpgp::XPrv;
|
||||||
|
let root_xprv = XPrv::new(mnemonic.generate_seed(None))?;
|
||||||
|
let account = DerivationIndex::new(*account_id, true)?;
|
||||||
|
let derived = root_xprv.derive_path(&opgp.derivation_path().chain_push(account))?;
|
||||||
|
if *public {
|
||||||
|
opgp.derive_public_with_xprv(writer, derived)?;
|
||||||
|
} else {
|
||||||
|
opgp.derive_with_xprv(writer, derived)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
derive::Derive {
|
||||||
|
command: derive::DeriveSubcommands::Key(key),
|
||||||
|
account_id,
|
||||||
|
public,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// HACK: We're abusing that we use the same key as OpenPGP. Maybe
|
||||||
|
// we should use ed25519_dalek.
|
||||||
|
use keyfork_derive_openpgp::XPrv;
|
||||||
|
let root_xprv = XPrv::new(mnemonic.generate_seed(None))?;
|
||||||
|
let account = DerivationIndex::new(*account_id, true)?;
|
||||||
|
let derived = root_xprv.derive_path(&key.derivation_path().chain_push(account))?;
|
||||||
|
if *public {
|
||||||
|
key.derive_public_with_xprv(writer, derived)?;
|
||||||
|
} else {
|
||||||
|
key.derive_with_xprv(writer, derived)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
impl MnemonicSubcommands {
|
impl MnemonicSubcommands {
|
||||||
pub fn handle(
|
pub fn handle(
|
||||||
&self,
|
&self,
|
||||||
_m: &Mnemonic,
|
_m: &Mnemonic,
|
||||||
_keyfork: &Keyfork,
|
_keyfork: &Keyfork,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
match self {
|
match self {
|
||||||
MnemonicSubcommands::Generate { source, size } => source.handle(size),
|
MnemonicSubcommands::Generate {
|
||||||
|
source,
|
||||||
|
size,
|
||||||
|
derive,
|
||||||
|
encrypt_to,
|
||||||
|
shard_to,
|
||||||
|
shard,
|
||||||
|
encrypt_to_self,
|
||||||
|
shard_to_self,
|
||||||
|
provision,
|
||||||
|
provision_count,
|
||||||
|
provision_config,
|
||||||
|
} => {
|
||||||
|
// NOTE: We should never have a case where there's Some() of empty vec, but
|
||||||
|
// we will make sure to check it just in case.
|
||||||
|
//
|
||||||
|
// We do not print the mnemonic if we are:
|
||||||
|
// * Encrypting to an existing, usable key
|
||||||
|
// * Encrypting to a newly provisioned key
|
||||||
|
// * Sharding to an existing Shardfile with usable keys
|
||||||
|
// * Sharding to existing, usable keys
|
||||||
|
// * Sharding to newly provisioned keys
|
||||||
|
let mut will_print_mnemonic =
|
||||||
|
encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(|e| e.is_empty());
|
||||||
|
will_print_mnemonic = will_print_mnemonic
|
||||||
|
&& (encrypt_to_self.as_ref().is_none() || provision.as_ref().is_none());
|
||||||
|
will_print_mnemonic = will_print_mnemonic && shard_to.is_none()
|
||||||
|
|| shard_to.as_ref().is_some_and(|s| s.is_empty());
|
||||||
|
will_print_mnemonic = will_print_mnemonic && shard.is_none()
|
||||||
|
|| shard.as_ref().is_some_and(|s| s.is_empty());
|
||||||
|
will_print_mnemonic = will_print_mnemonic && shard_to_self.is_none();
|
||||||
|
|
||||||
|
let mnemonic = source.handle(size)?;
|
||||||
|
|
||||||
|
if let Some(derive) = derive {
|
||||||
|
let stdout = std::io::stdout();
|
||||||
|
if will_print_mnemonic && !stdout.is_terminal() {
|
||||||
|
eprintln!(
|
||||||
|
"Writing plaintext mnemonic and derivation output to standard output"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
do_derive(&mnemonic, derive)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(encrypt_to) = encrypt_to {
|
||||||
|
for entry in encrypt_to {
|
||||||
|
do_encrypt_to(&mnemonic, &entry.inner, &entry.values)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(encrypt_to_self) = encrypt_to_self {
|
||||||
|
let mut accounts: std::collections::HashSet<u32> = Default::default();
|
||||||
|
if let Some(provision::Provision {
|
||||||
|
provisioner_name: provision::Provisioner::OpenPGPCard(_),
|
||||||
|
account_id,
|
||||||
|
..
|
||||||
|
}) = provision
|
||||||
|
{
|
||||||
|
accounts.insert(*account_id);
|
||||||
|
}
|
||||||
|
if let Some(derive::Derive {
|
||||||
|
command: derive::DeriveSubcommands::OpenPGP(_),
|
||||||
|
account_id,
|
||||||
|
..
|
||||||
|
}) = derive
|
||||||
|
{
|
||||||
|
accounts.insert(*account_id);
|
||||||
|
}
|
||||||
|
let indices = accounts
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| DerivationIndex::new(i, true))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
assert!(
|
||||||
|
!indices.is_empty(),
|
||||||
|
"neither derived nor provisioned accounts were found"
|
||||||
|
);
|
||||||
|
do_encrypt_to_self(&mnemonic, &encrypt_to_self, &indices)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(shard_to_self) = shard_to_self {
|
||||||
|
do_shard_to_self(&mnemonic, &shard_to_self.inner, &shard_to_self.values)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(provisioner) = provision {
|
||||||
|
// determine if we should write to standard output based on whether we have a
|
||||||
|
// matching pair of provisioner and public derivation output.
|
||||||
|
let mut will_output_public_key = true;
|
||||||
|
|
||||||
|
if let Some(derive) = derive {
|
||||||
|
let matches = match (provisioner, derive) {
|
||||||
|
(
|
||||||
|
provision::Provision {
|
||||||
|
provisioner_name: provision::Provisioner::OpenPGPCard(_),
|
||||||
|
account_id: p_id,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
derive::Derive {
|
||||||
|
command: derive::DeriveSubcommands::OpenPGP(_),
|
||||||
|
account_id: d_id,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => p_id == d_id,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if matches && derive.public {
|
||||||
|
will_output_public_key = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values = provision_config.values.clone();
|
||||||
|
if !will_output_public_key && !values.contains_key("output") {
|
||||||
|
values.insert(String::from("_skip_cert_output"), String::from("1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
do_provision(&mnemonic, provisioner, *provision_count, &values)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(shard_to) = shard_to {
|
||||||
|
for entry in shard_to {
|
||||||
|
do_shard_to(&mnemonic, &entry.inner, &entry.values)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(shard) = shard {
|
||||||
|
for entry in shard {
|
||||||
|
do_shard(&mnemonic, &entry.inner, &entry.values)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if will_print_mnemonic {
|
||||||
|
println!("{}", mnemonic);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,11 @@ mod mnemonic;
|
||||||
mod provision;
|
mod provision;
|
||||||
mod recover;
|
mod recover;
|
||||||
mod shard;
|
mod shard;
|
||||||
mod wizard;
|
|
||||||
|
pub fn create(path: &std::path::Path) -> std::io::Result<std::fs::File> {
|
||||||
|
eprintln!("Writing derived key to: {path}", path=path.display());
|
||||||
|
std::fs::File::create(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// The Kitchen Sink of Entropy.
|
/// The Kitchen Sink of Entropy.
|
||||||
#[derive(Parser, Clone, Debug)]
|
#[derive(Parser, Clone, Debug)]
|
||||||
|
@ -57,9 +61,6 @@ pub enum KeyforkCommands {
|
||||||
/// leaked by any individual deriver.
|
/// leaked by any individual deriver.
|
||||||
Recover(recover::Recover),
|
Recover(recover::Recover),
|
||||||
|
|
||||||
/// Utilities to automatically manage the setup of Keyfork.
|
|
||||||
Wizard(wizard::Wizard),
|
|
||||||
|
|
||||||
/// Print an autocompletion file to standard output.
|
/// Print an autocompletion file to standard output.
|
||||||
///
|
///
|
||||||
/// Keyfork does not manage the installation of completion files. Consult the documentation for
|
/// Keyfork does not manage the installation of completion files. Consult the documentation for
|
||||||
|
@ -79,8 +80,7 @@ impl KeyforkCommands {
|
||||||
d.handle(keyfork)?;
|
d.handle(keyfork)?;
|
||||||
}
|
}
|
||||||
KeyforkCommands::Mnemonic(m) => {
|
KeyforkCommands::Mnemonic(m) => {
|
||||||
let response = m.command.handle(m, keyfork)?;
|
m.command.handle(m, keyfork)?;
|
||||||
println!("{response}");
|
|
||||||
}
|
}
|
||||||
KeyforkCommands::Shard(s) => {
|
KeyforkCommands::Shard(s) => {
|
||||||
s.command.handle(s, keyfork)?;
|
s.command.handle(s, keyfork)?;
|
||||||
|
@ -91,9 +91,6 @@ impl KeyforkCommands {
|
||||||
KeyforkCommands::Recover(r) => {
|
KeyforkCommands::Recover(r) => {
|
||||||
r.handle(keyfork)?;
|
r.handle(keyfork)?;
|
||||||
}
|
}
|
||||||
KeyforkCommands::Wizard(w) => {
|
|
||||||
w.handle(keyfork)?;
|
|
||||||
}
|
|
||||||
#[cfg(feature = "completion")]
|
#[cfg(feature = "completion")]
|
||||||
KeyforkCommands::Completion { shell } => {
|
KeyforkCommands::Completion { shell } => {
|
||||||
let mut command = Keyfork::command();
|
let mut command = Keyfork::command();
|
||||||
|
|
|
@ -3,81 +3,135 @@ use crate::config;
|
||||||
|
|
||||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||||
|
|
||||||
|
use keyfork_derive_util::{DerivationIndex, ExtendedPrivateKey};
|
||||||
|
|
||||||
|
mod openpgp;
|
||||||
|
|
||||||
|
type Identifier = (String, Option<String>);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Provisioner {
|
pub enum Provisioner {
|
||||||
OpenPGPCard(OpenPGPCard),
|
OpenPGPCard(openpgp::OpenPGPCard),
|
||||||
|
Shard(openpgp::Shard),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Provisioner {
|
impl std::fmt::Display for Provisioner {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
f.write_str(self.identifier())
|
||||||
Provisioner::OpenPGPCard(_) => f.write_str("openpgp-card"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Provisioner {
|
impl Provisioner {
|
||||||
fn discover(&self) -> Vec<(String, Option<String>)> {
|
pub fn identifier(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Provisioner::OpenPGPCard(o) => o.discover(),
|
Provisioner::OpenPGPCard(_) => "openpgp-card",
|
||||||
|
Provisioner::Shard(_) => "shard",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn provision(
|
pub fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
|
||||||
|
match self {
|
||||||
|
Provisioner::OpenPGPCard(o) => o.discover(),
|
||||||
|
Provisioner::Shard(s) => s.discover(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn provision(
|
||||||
&self,
|
&self,
|
||||||
provisioner: config::Provisioner,
|
provisioner: config::Provisioner,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
match self {
|
match self {
|
||||||
Provisioner::OpenPGPCard(o) => o.provision(provisioner),
|
Provisioner::OpenPGPCard(o) => {
|
||||||
|
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
|
||||||
|
type XPrv = ExtendedPrivateKey<Prv>;
|
||||||
|
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
||||||
|
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
|
||||||
|
.chain_push(account_index);
|
||||||
|
let mut client = keyforkd_client::Client::discover_socket()?;
|
||||||
|
let xprv: XPrv = client.request_xprv(&path)?;
|
||||||
|
o.provision(xprv, provisioner)
|
||||||
|
}
|
||||||
|
Provisioner::Shard(s) => {
|
||||||
|
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
|
||||||
|
type XPrv = ExtendedPrivateKey<Prv>;
|
||||||
|
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
||||||
|
let path = <openpgp::Shard as ProvisionExec>::derivation_prefix()
|
||||||
|
.chain_push(account_index);
|
||||||
|
let mut client = keyforkd_client::Client::discover_socket()?;
|
||||||
|
let xprv: XPrv = client.request_xprv(&path)?;
|
||||||
|
s.provision(xprv, provisioner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn provision_with_mnemonic(
|
||||||
|
&self,
|
||||||
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
|
provisioner: config::Provisioner,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
match self {
|
||||||
|
Provisioner::OpenPGPCard(o) => {
|
||||||
|
type Prv = <openpgp::OpenPGPCard as ProvisionExec>::PrivateKey;
|
||||||
|
type XPrv = ExtendedPrivateKey<Prv>;
|
||||||
|
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
||||||
|
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
|
||||||
|
.chain_push(account_index);
|
||||||
|
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
|
||||||
|
o.provision(xprv, provisioner)
|
||||||
|
}
|
||||||
|
Provisioner::Shard(s) => {
|
||||||
|
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
|
||||||
|
type XPrv = ExtendedPrivateKey<Prv>;
|
||||||
|
let account_index = DerivationIndex::new(provisioner.account, true)?;
|
||||||
|
let path = <openpgp::Shard as ProvisionExec>::derivation_prefix()
|
||||||
|
.chain_push(account_index);
|
||||||
|
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
|
||||||
|
s.provision(xprv, provisioner)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValueEnum for Provisioner {
|
impl ValueEnum for Provisioner {
|
||||||
fn value_variants<'a>() -> &'a [Self] {
|
fn value_variants<'a>() -> &'a [Self] {
|
||||||
&[Self::OpenPGPCard(OpenPGPCard)]
|
&[Self::OpenPGPCard(openpgp::OpenPGPCard), Self::Shard(openpgp::Shard)]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_possible_value(&self) -> Option<PossibleValue> {
|
fn to_possible_value(&self) -> Option<PossibleValue> {
|
||||||
Some(PossibleValue::new(match self {
|
Some(PossibleValue::new(self.identifier()))
|
||||||
Self::OpenPGPCard(_) => "openpgp-card",
|
}
|
||||||
}))
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[error("The given value could not be matched as a provisioner: {0} ({1})")]
|
||||||
|
pub struct ProvisionerFromStrError(String, String);
|
||||||
|
|
||||||
|
impl std::str::FromStr for Provisioner {
|
||||||
|
type Err = ProvisionerFromStrError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
<Provisioner as ValueEnum>::from_str(s, false)
|
||||||
|
.map_err(|e| ProvisionerFromStrError(s.to_string(), e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ProvisionExec {
|
trait ProvisionExec {
|
||||||
|
type PrivateKey: keyfork_derive_util::PrivateKey + Clone;
|
||||||
|
|
||||||
/// Discover all known places the formatted key can be deployed to.
|
/// Discover all known places the formatted key can be deployed to.
|
||||||
fn discover(&self) -> Vec<(String, Option<String>)> {
|
fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
|
||||||
vec![]
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the derivation path for deriving keys.
|
||||||
|
fn derivation_prefix() -> keyfork_derive_util::DerivationPath;
|
||||||
|
|
||||||
/// Derive a key and deploy it to a target.
|
/// Derive a key and deploy it to a target.
|
||||||
fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>;
|
fn provision(
|
||||||
}
|
&self,
|
||||||
|
xprv: keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
|
||||||
#[derive(Clone, Debug)]
|
p: config::Provisioner,
|
||||||
pub struct OpenPGPCard;
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
impl ProvisionExec for OpenPGPCard {
|
|
||||||
fn discover(&self) -> Vec<(String, Option<String>)> {
|
|
||||||
/*
|
|
||||||
vec![
|
|
||||||
(
|
|
||||||
"0006:26144195".to_string(),
|
|
||||||
Some("Yubicats Heywood".to_string()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"0006:2614419y".to_string(),
|
|
||||||
Some("Yubicats Heywood".to_string()),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
*/
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn provision(&self, _p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
|
@ -94,15 +148,27 @@ pub struct Provision {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub subcommand: Option<ProvisionSubcommands>,
|
pub subcommand: Option<ProvisionSubcommands>,
|
||||||
|
|
||||||
provisioner_name: Provisioner,
|
pub provisioner_name: Provisioner,
|
||||||
|
|
||||||
/// Account ID.
|
/// Account ID.
|
||||||
#[arg(long, required(true))]
|
#[arg(long, default_value = "0")]
|
||||||
account_id: Option<u32>,
|
pub account_id: u32,
|
||||||
|
|
||||||
/// Identifier of the hardware to deploy to, listable by running the `discover` subcommand.
|
/// Identifier of the hardware to deploy to, listable by running the `discover` subcommand.
|
||||||
#[arg(long, required(true))]
|
#[arg(long)]
|
||||||
identifier: Option<String>,
|
pub identifier: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for Provision {
|
||||||
|
type Err = clap::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
Provision::try_parse_from(
|
||||||
|
[String::from("provision")]
|
||||||
|
.into_iter()
|
||||||
|
.chain(shlex::Shlex::new(s)),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: In the future, this impl will be used by `keyfork recover` to reprovision hardware from
|
// NOTE: In the future, this impl will be used by `keyfork recover` to reprovision hardware from
|
||||||
|
@ -118,8 +184,7 @@ impl TryFrom<Provision> for config::Provisioner {
|
||||||
|
|
||||||
fn try_from(value: Provision) -> Result<Self, Self::Error> {
|
fn try_from(value: Provision) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
name: value.provisioner_name.to_string(),
|
account: value.account_id,
|
||||||
account: value.account_id.ok_or(MissingField("account_id"))?,
|
|
||||||
identifier: value.identifier.ok_or(MissingField("identifier"))?,
|
identifier: value.identifier.ok_or(MissingField("identifier"))?,
|
||||||
metadata: Default::default(),
|
metadata: Default::default(),
|
||||||
})
|
})
|
||||||
|
@ -130,7 +195,7 @@ impl Provision {
|
||||||
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
match self.subcommand {
|
match self.subcommand {
|
||||||
Some(ProvisionSubcommands::Discover) => {
|
Some(ProvisionSubcommands::Discover) => {
|
||||||
let mut iter = self.provisioner_name.discover().into_iter().peekable();
|
let mut iter = self.provisioner_name.discover()?.into_iter().peekable();
|
||||||
while let Some((identifier, context)) = iter.next() {
|
while let Some((identifier, context)) = iter.next() {
|
||||||
println!("Identifier: {identifier}");
|
println!("Identifier: {identifier}");
|
||||||
if let Some(context) = context {
|
if let Some(context) = context {
|
||||||
|
@ -142,7 +207,21 @@ impl Provision {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.provisioner_name.provision(self.clone().try_into()?)?;
|
let provisioner_with_identifier = match self.identifier {
|
||||||
|
Some(_) => self.clone(),
|
||||||
|
None => {
|
||||||
|
let identifiers = self.provisioner_name.discover()?;
|
||||||
|
let [id] = &identifiers[..] else {
|
||||||
|
panic!("invalid amount of identifiers; pass --identifier");
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
identifier: Some(id.0.clone()),
|
||||||
|
..self.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let config = config::Provisioner::try_from(provisioner_with_identifier)?;
|
||||||
|
self.provisioner_name.provision(config)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
use super::ProvisionExec;
|
||||||
|
use crate::{
|
||||||
|
config,
|
||||||
|
openpgp_card::{factory_reset_current_card, get_new_pins},
|
||||||
|
};
|
||||||
|
|
||||||
|
use card_backend_pcsc::PcscBackend;
|
||||||
|
use keyfork_derive_openpgp::{
|
||||||
|
openpgp::{
|
||||||
|
armor::{Kind, Writer},
|
||||||
|
packet::UserID,
|
||||||
|
serialize::Serialize,
|
||||||
|
types::KeyFlags,
|
||||||
|
},
|
||||||
|
XPrv,
|
||||||
|
};
|
||||||
|
use keyfork_prompt::default_handler;
|
||||||
|
use openpgp_card_sequoia::{state::Open, Card};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
#[error("Provisioner was unable to find a matching smartcard")]
|
||||||
|
struct NoMatchingSmartcard;
|
||||||
|
|
||||||
|
fn discover_cards() -> Result<Vec<(String, Option<String>)>, Box<dyn std::error::Error>> {
|
||||||
|
let mut idents = vec![];
|
||||||
|
for backend in PcscBackend::cards(None)? {
|
||||||
|
let backend = backend?;
|
||||||
|
let mut card = Card::<Open>::new(backend)?;
|
||||||
|
let mut transaction = card.transaction()?;
|
||||||
|
let identifier = transaction.application_identifier()?.ident();
|
||||||
|
let name = transaction.cardholder_name()?;
|
||||||
|
let name = (!name.is_empty()).then_some(name);
|
||||||
|
idents.push((identifier, name));
|
||||||
|
}
|
||||||
|
Ok(idents)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provision_card(
|
||||||
|
provisioner: config::Provisioner,
|
||||||
|
xprv: XPrv,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut pm = default_handler()?;
|
||||||
|
|
||||||
|
let (user_pin, admin_pin) = get_new_pins(&mut *pm)?;
|
||||||
|
|
||||||
|
let subkeys = vec![
|
||||||
|
KeyFlags::empty().set_certification(),
|
||||||
|
KeyFlags::empty().set_signing(),
|
||||||
|
KeyFlags::empty()
|
||||||
|
.set_transport_encryption()
|
||||||
|
.set_storage_encryption(),
|
||||||
|
KeyFlags::empty().set_authentication(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let userid = match provisioner.metadata.as_ref().and_then(|m| m.get("userid")) {
|
||||||
|
Some(userid) => UserID::from(userid.as_str()),
|
||||||
|
None => UserID::from("Keyfork-Provisioned Key"),
|
||||||
|
};
|
||||||
|
let cert = keyfork_derive_openpgp::derive(xprv.clone(), &subkeys, &userid)?;
|
||||||
|
|
||||||
|
if !provisioner
|
||||||
|
.metadata
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|m| m.contains_key("_skip_cert_output"))
|
||||||
|
{
|
||||||
|
let cert_output = match provisioner.metadata.as_ref().and_then(|m| m.get("output")) {
|
||||||
|
Some(cert_output) => PathBuf::from(cert_output),
|
||||||
|
None => {
|
||||||
|
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
|
||||||
|
eprintln!(
|
||||||
|
"Writing OpenPGP certificate to: {path}",
|
||||||
|
path = path.display()
|
||||||
|
);
|
||||||
|
path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let cert_output_file = std::fs::File::create(cert_output)?;
|
||||||
|
let mut writer = Writer::new(cert_output_file, Kind::PublicKey)?;
|
||||||
|
cert.serialize(&mut writer)?;
|
||||||
|
writer.finalize()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut has_provisioned = false;
|
||||||
|
|
||||||
|
for backend in PcscBackend::cards(None)? {
|
||||||
|
let backend = backend?;
|
||||||
|
|
||||||
|
let result = factory_reset_current_card(
|
||||||
|
&mut |identifier| identifier == provisioner.identifier,
|
||||||
|
user_pin.trim(),
|
||||||
|
admin_pin.trim(),
|
||||||
|
&cert,
|
||||||
|
&keyfork_derive_openpgp::openpgp::policy::StandardPolicy::new(),
|
||||||
|
backend,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
has_provisioned = has_provisioned || result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_provisioned {
|
||||||
|
return Err(NoMatchingSmartcard)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct OpenPGPCard;
|
||||||
|
|
||||||
|
impl ProvisionExec for OpenPGPCard {
|
||||||
|
type PrivateKey = keyfork_derive_openpgp::XPrvKey;
|
||||||
|
|
||||||
|
fn discover(&self) -> Result<Vec<(String, Option<String>)>, Box<dyn std::error::Error>> {
|
||||||
|
discover_cards()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derivation_prefix() -> keyfork_derive_util::DerivationPath {
|
||||||
|
keyfork_derive_path_data::paths::OPENPGP.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provision(
|
||||||
|
&self,
|
||||||
|
xprv: XPrv,
|
||||||
|
provisioner: config::Provisioner,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
provision_card(provisioner, xprv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Shard;
|
||||||
|
|
||||||
|
impl ProvisionExec for Shard {
|
||||||
|
type PrivateKey = keyfork_derive_openpgp::XPrvKey;
|
||||||
|
|
||||||
|
fn discover(&self) -> Result<Vec<(String, Option<String>)>, Box<dyn std::error::Error>> {
|
||||||
|
discover_cards()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derivation_prefix() -> keyfork_derive_util::DerivationPath {
|
||||||
|
keyfork_derive_path_data::paths::OPENPGP_SHARD.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provision(
|
||||||
|
&self,
|
||||||
|
xprv: XPrv,
|
||||||
|
provisioner: config::Provisioner,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
provision_card(provisioner, xprv)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
use super::Keyfork;
|
use super::Keyfork;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use nix::{
|
||||||
|
sys::wait::waitpid,
|
||||||
|
unistd::{fork, ForkResult},
|
||||||
|
};
|
||||||
|
|
||||||
use keyfork_mnemonic::{English, Mnemonic};
|
use keyfork_mnemonic::{English, Mnemonic};
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
|
@ -80,12 +84,32 @@ impl RecoverSubcommands {
|
||||||
pub struct Recover {
|
pub struct Recover {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: RecoverSubcommands,
|
command: RecoverSubcommands,
|
||||||
|
|
||||||
|
/// Daemonize the server once started, restoring control back to the shell.
|
||||||
|
#[arg(long, global=true)]
|
||||||
|
daemon: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Recover {
|
impl Recover {
|
||||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||||
let seed = self.command.handle()?;
|
let seed = self.command.handle()?;
|
||||||
let mnemonic = Mnemonic::try_from_slice(&seed)?;
|
let mnemonic = Mnemonic::try_from_slice(&seed)?;
|
||||||
|
if self.daemon {
|
||||||
|
// SAFETY: Forking threaded programs is unsafe. We know we don't have multiple
|
||||||
|
// threads at this point.
|
||||||
|
match unsafe { fork() }? {
|
||||||
|
ForkResult::Parent { child } => {
|
||||||
|
// wait for the child to die, so we don't exit prematurely
|
||||||
|
waitpid(Some(child), None)?;
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
ForkResult::Child => {
|
||||||
|
if let ForkResult::Parent { .. } = unsafe { fork() }? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -50,6 +50,14 @@ trait ShardExec {
|
||||||
key_discovery: Option<&Path>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
fn metadata(
|
||||||
|
&self,
|
||||||
|
key_discovery: Option<&Path>,
|
||||||
|
input: impl Read + Send + Sync,
|
||||||
|
output_pubkeys: &mut impl Write,
|
||||||
|
output: &mut impl Write,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -92,6 +100,31 @@ impl ShardExec for OpenPGP {
|
||||||
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
|
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn metadata(
|
||||||
|
&self,
|
||||||
|
key_discovery: Option<&Path>,
|
||||||
|
input: impl Read + Send + Sync,
|
||||||
|
output_pubkeys: &mut impl Write,
|
||||||
|
output: &mut impl Write,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use keyfork_derive_openpgp::openpgp::{
|
||||||
|
serialize::Marshal,
|
||||||
|
armor::{Writer, Kind},
|
||||||
|
};
|
||||||
|
|
||||||
|
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
|
let prompt = default_handler()?;
|
||||||
|
|
||||||
|
let (threshold, certs) = openpgp.decrypt_metadata_from_file(key_discovery, input, prompt)?;
|
||||||
|
let mut writer = Writer::new(output_pubkeys, Kind::PublicKey)?;
|
||||||
|
for cert in certs {
|
||||||
|
cert.serialize(&mut writer)?;
|
||||||
|
}
|
||||||
|
writer.finalize()?;
|
||||||
|
writeln!(output, "Threshold: {threshold}")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -141,6 +174,20 @@ pub enum ShardSubcommands {
|
||||||
/// The path to discover private keys from.
|
/// The path to discover private keys from.
|
||||||
key_discovery: Option<PathBuf>,
|
key_discovery: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Decrypt metadata for a shardfile, including the threshold and the public keys. Public keys
|
||||||
|
/// are serialized to a file.
|
||||||
|
Metadata {
|
||||||
|
/// The path to load the Shardfile from.
|
||||||
|
shardfile: PathBuf,
|
||||||
|
|
||||||
|
/// The path to write public keys to.
|
||||||
|
#[arg(long)]
|
||||||
|
output_pubkeys: PathBuf,
|
||||||
|
|
||||||
|
/// The path to discover private keys from.
|
||||||
|
key_discovery: Option<PathBuf>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShardSubcommands {
|
impl ShardSubcommands {
|
||||||
|
@ -209,6 +256,27 @@ impl ShardSubcommands {
|
||||||
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ShardSubcommands::Metadata { shardfile, output_pubkeys, key_discovery } => {
|
||||||
|
let shard_content = std::fs::read_to_string(shardfile)?;
|
||||||
|
if shard_content.contains("BEGIN PGP MESSAGE") {
|
||||||
|
let _ = format.insert(Format::OpenPGP(OpenPGP));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut output_pubkeys_file = std::fs::File::create(output_pubkeys)?;
|
||||||
|
|
||||||
|
match format {
|
||||||
|
Some(Format::OpenPGP(o)) => o.metadata(
|
||||||
|
key_discovery.as_deref(),
|
||||||
|
shard_content.as_bytes(),
|
||||||
|
&mut output_pubkeys_file,
|
||||||
|
&mut stdout,
|
||||||
|
),
|
||||||
|
Some(Format::P256(_p)) => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,314 +0,0 @@
|
||||||
use super::Keyfork;
|
|
||||||
use clap::{Args, 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_path_data::paths;
|
|
||||||
use keyfork_derive_util::DerivationIndex;
|
|
||||||
use keyfork_mnemonic::Mnemonic;
|
|
||||||
use keyfork_prompt::{
|
|
||||||
default_handler,
|
|
||||||
prompt_validated_passphrase,
|
|
||||||
validators::{SecurePinValidator, Validator},
|
|
||||||
Message,
|
|
||||||
};
|
|
||||||
|
|
||||||
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> {
|
|
||||||
let subkeys = vec![
|
|
||||||
KeyFlags::empty().set_certification(),
|
|
||||||
KeyFlags::empty().set_signing(),
|
|
||||||
KeyFlags::empty()
|
|
||||||
.set_transport_encryption()
|
|
||||||
.set_storage_encryption(),
|
|
||||||
KeyFlags::empty().set_authentication(),
|
|
||||||
];
|
|
||||||
|
|
||||||
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 userid = UserID::from(format!("Keyfork Shard {index}"));
|
|
||||||
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
|
|
||||||
Ok(cert)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: extract into crate
|
|
||||||
/// Factory reset the current card so long as it does not match the last-used backend.
|
|
||||||
fn factory_reset_current_card(
|
|
||||||
seen_cards: &mut HashSet<String>,
|
|
||||||
user_pin: &str,
|
|
||||||
admin_pin: &str,
|
|
||||||
cert: &Cert,
|
|
||||||
card_backend: PcscBackend,
|
|
||||||
) -> Result<()> {
|
|
||||||
let policy = openpgp::policy::NullPolicy::new();
|
|
||||||
let valid_cert = cert.with_policy(&policy, None)?;
|
|
||||||
let signing_key = valid_cert
|
|
||||||
.keys()
|
|
||||||
.for_signing()
|
|
||||||
.secret()
|
|
||||||
.next()
|
|
||||||
.expect("no signing key found");
|
|
||||||
let decryption_key = valid_cert
|
|
||||||
.keys()
|
|
||||||
.for_storage_encryption()
|
|
||||||
.secret()
|
|
||||||
.next()
|
|
||||||
.expect("no decryption key found");
|
|
||||||
let authentication_key = valid_cert
|
|
||||||
.keys()
|
|
||||||
.for_authentication()
|
|
||||||
.secret()
|
|
||||||
.next()
|
|
||||||
.expect("no authentication key found");
|
|
||||||
let mut card = Card::<Open>::new(card_backend)?;
|
|
||||||
let mut transaction = card.transaction()?;
|
|
||||||
let application_identifier = transaction.application_identifier()?.ident();
|
|
||||||
if seen_cards.contains(&application_identifier) {
|
|
||||||
// we were given the same card, error
|
|
||||||
panic!("Previously used card {application_identifier} was reused");
|
|
||||||
} else {
|
|
||||||
seen_cards.insert(application_identifier);
|
|
||||||
}
|
|
||||||
transaction.factory_reset()?;
|
|
||||||
let mut admin = transaction.to_admin_card("12345678")?;
|
|
||||||
admin.upload_key(signing_key, KeyType::Signing, None)?;
|
|
||||||
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
|
|
||||||
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
|
|
||||||
transaction.change_user_pin("123456", user_pin)?;
|
|
||||||
transaction.change_admin_pin("12345678", admin_pin)?;
|
|
||||||
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.
|
|
||||||
#[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 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WizardSubcommands {
|
|
||||||
// dispatch
|
|
||||||
fn handle(&self) -> Result<()> {
|
|
||||||
match self {
|
|
||||||
WizardSubcommands::GenerateShardSecret(gss) => gss.handle(),
|
|
||||||
WizardSubcommands::BottomsUp(bu) => bu.handle(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GenerateShardSecret {
|
|
||||||
fn handle(&self) -> Result<()> {
|
|
||||||
let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
|
|
||||||
let mut pm = default_handler()?;
|
|
||||||
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 = prompt_validated_passphrase(
|
|
||||||
&mut *pm,
|
|
||||||
"Please enter the new smartcard User PIN: ",
|
|
||||||
3,
|
|
||||||
&user_pin_validator,
|
|
||||||
)?;
|
|
||||||
let admin_pin = prompt_validated_passphrase(
|
|
||||||
&mut *pm,
|
|
||||||
"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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
let certs = OpenPGP::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)]
|
|
||||||
command: WizardSubcommands,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Wizard {
|
|
||||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
|
||||||
self.command.handle()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,20 +2,19 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Mnemonic {
|
pub struct Mnemonic {
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Provisioner {
|
pub struct Provisioner {
|
||||||
pub name: String,
|
|
||||||
pub account: u32,
|
pub account: u32,
|
||||||
pub identifier: String,
|
pub identifier: String,
|
||||||
pub metadata: Option<HashMap<String, String>>,
|
pub metadata: Option<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub mnemonic: Mnemonic,
|
pub mnemonic: Mnemonic,
|
||||||
pub provisioner: Vec<Provisioner>,
|
pub provisioner: Vec<Provisioner>,
|
||||||
|
|
|
@ -10,6 +10,8 @@ use keyfork_bin::{Bin, ClosureBin};
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
|
pub mod clap_ext;
|
||||||
|
mod openpgp_card;
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let bin = ClosureBin::new(|| {
|
let bin = ClosureBin::new(|| {
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
use card_backend_pcsc::PcscBackend;
|
||||||
|
use keyfork_derive_openpgp::openpgp::{policy::Policy, Cert};
|
||||||
|
use keyfork_prompt::{
|
||||||
|
prompt_validated_passphrase,
|
||||||
|
validators::{SecurePinValidator, Validator},
|
||||||
|
Message, PromptHandler,
|
||||||
|
};
|
||||||
|
use openpgp_card_sequoia::{state::Open, types::KeyType, types::TouchPolicy, Card};
|
||||||
|
|
||||||
|
pub fn get_new_pins(
|
||||||
|
pm: &mut dyn PromptHandler,
|
||||||
|
) -> Result<(String, String), Box<dyn std::error::Error>> {
|
||||||
|
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();
|
||||||
|
|
||||||
|
let user_pin = loop {
|
||||||
|
let user_pin = prompt_validated_passphrase(
|
||||||
|
&mut *pm,
|
||||||
|
"Please enter the new smartcard User PIN: ",
|
||||||
|
3,
|
||||||
|
&user_pin_validator,
|
||||||
|
)?;
|
||||||
|
let validated_user_pin = prompt_validated_passphrase(
|
||||||
|
&mut *pm,
|
||||||
|
"Please verify the new smartcard User PIN: ",
|
||||||
|
3,
|
||||||
|
&user_pin_validator,
|
||||||
|
)?;
|
||||||
|
if user_pin != validated_user_pin {
|
||||||
|
pm.prompt_message(Message::Text("User PINs did not match. Retrying.".into()))?;
|
||||||
|
} else {
|
||||||
|
break user_pin;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let admin_pin = loop {
|
||||||
|
let admin_pin = prompt_validated_passphrase(
|
||||||
|
&mut *pm,
|
||||||
|
"Please enter the new smartcard Admin PIN: ",
|
||||||
|
3,
|
||||||
|
&admin_pin_validator,
|
||||||
|
)?;
|
||||||
|
let validated_admin_pin = prompt_validated_passphrase(
|
||||||
|
&mut *pm,
|
||||||
|
"Please verify the new smartcard Admin PIN: ",
|
||||||
|
3,
|
||||||
|
&admin_pin_validator,
|
||||||
|
)?;
|
||||||
|
if admin_pin != validated_admin_pin {
|
||||||
|
pm.prompt_message(Message::Text("Admin PINs did not match. Retrying.".into()))?;
|
||||||
|
} else {
|
||||||
|
break admin_pin;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((user_pin, admin_pin))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Factory reset the current card so long as it does not match the last-used backend.
|
||||||
|
///
|
||||||
|
/// The return value of `false` means the filter was matched, whereas `true` means it was
|
||||||
|
/// successfully provisioned.
|
||||||
|
pub fn factory_reset_current_card(
|
||||||
|
card_filter: &mut dyn FnMut(String) -> bool,
|
||||||
|
user_pin: &str,
|
||||||
|
admin_pin: &str,
|
||||||
|
cert: &Cert,
|
||||||
|
policy: &dyn Policy,
|
||||||
|
card_backend: PcscBackend,
|
||||||
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
let valid_cert = cert.with_policy(policy, None)?;
|
||||||
|
let signing_key = valid_cert
|
||||||
|
.keys()
|
||||||
|
.for_signing()
|
||||||
|
.secret()
|
||||||
|
.next()
|
||||||
|
.expect("no signing key found");
|
||||||
|
let decryption_key = valid_cert
|
||||||
|
.keys()
|
||||||
|
.for_storage_encryption()
|
||||||
|
.secret()
|
||||||
|
.next()
|
||||||
|
.expect("no decryption key found");
|
||||||
|
let authentication_key = valid_cert
|
||||||
|
.keys()
|
||||||
|
.for_authentication()
|
||||||
|
.secret()
|
||||||
|
.next()
|
||||||
|
.expect("no authentication key found");
|
||||||
|
let mut card = Card::<Open>::new(card_backend)?;
|
||||||
|
let mut transaction = card.transaction()?;
|
||||||
|
let application_identifier = transaction.application_identifier()?.ident();
|
||||||
|
if !card_filter(application_identifier) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
transaction.factory_reset()?;
|
||||||
|
let mut admin = transaction.to_admin_card("12345678")?;
|
||||||
|
admin.upload_key(signing_key, KeyType::Signing, None)?;
|
||||||
|
admin.set_touch_policy(KeyType::Signing, TouchPolicy::On)?;
|
||||||
|
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
|
||||||
|
admin.set_touch_policy(KeyType::Decryption, TouchPolicy::On)?;
|
||||||
|
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
|
||||||
|
admin.set_touch_policy(KeyType::Authentication, TouchPolicy::On)?;
|
||||||
|
transaction.change_user_pin("123456", user_pin)?;
|
||||||
|
transaction.change_admin_pin("12345678", admin_pin)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-qrcode"
|
name = "keyfork-qrcode"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
repository = "https://git.distrust.co/public/keyfork"
|
repository = "https://git.distrust.co/public/keyfork"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -15,8 +15,9 @@ decode-backend-zbar = ["dep:keyfork-zbar"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-bug = { workspace = true }
|
keyfork-bug = { workspace = true }
|
||||||
keyfork-zbar = { workspace = true, optional = true }
|
keyfork-zbar = { workspace = true, optional = true, features = ["image"] }
|
||||||
image = { workspace = true, default-features = false, features = ["jpeg"] }
|
image = { workspace = true, default-features = false, features = ["jpeg"] }
|
||||||
rqrr = { version = "0.7.0", optional = true }
|
rqrr = { version = "0.9.0", optional = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
v4l = { workspace = true }
|
v4l = { workspace = true }
|
||||||
|
cfg-if = "1.0.0"
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::time::Duration;
|
||||||
use keyfork_qrcode::scan_camera;
|
use keyfork_qrcode::scan_camera;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let output = scan_camera(Duration::from_secs(60 * 10), 0)?;
|
let output = scan_camera(Duration::from_secs(15), 0)?;
|
||||||
if let Some(scanned_text) = output {
|
if let Some(scanned_text) = output {
|
||||||
println!("{scanned_text}");
|
println!("{scanned_text}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,17 @@
|
||||||
|
|
||||||
use keyfork_bug as bug;
|
use keyfork_bug as bug;
|
||||||
|
|
||||||
use image::ImageReader;
|
use image::{ImageBuffer, ImageReader, Luma};
|
||||||
use std::{
|
use std::{
|
||||||
io::{Cursor, Write},
|
io::{Cursor, Write},
|
||||||
time::{Duration, Instant},
|
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use v4l::{
|
use v4l::{
|
||||||
buffer::Type,
|
buffer::Type,
|
||||||
io::{userptr::Stream, traits::CaptureStream},
|
io::{traits::CaptureStream, userptr::Stream},
|
||||||
video::Capture,
|
video::Capture,
|
||||||
FourCC,
|
Device, FourCC,
|
||||||
Device,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A QR code could not be generated.
|
/// A QR code could not be generated.
|
||||||
|
@ -102,70 +101,117 @@ pub fn qrencode(
|
||||||
|
|
||||||
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
|
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
|
||||||
|
|
||||||
|
trait Scanner {
|
||||||
|
fn scan_image(&mut self, image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "decode-backend-zbar")]
|
||||||
|
mod zbar {
|
||||||
|
use super::{ImageBuffer, Luma, Scanner};
|
||||||
|
|
||||||
|
pub struct Zbar {
|
||||||
|
scanner: keyfork_zbar::image_scanner::ImageScanner,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Zbar {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Zbar {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
scanner: keyfork_zbar::image_scanner::ImageScanner::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scanner for Zbar {
|
||||||
|
fn scan_image(
|
||||||
|
&mut self,
|
||||||
|
image: ImageBuffer<Luma<u8>, Vec<u8>>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let image = keyfork_zbar::image::Image::from(image);
|
||||||
|
self.scanner.scan_image(&image).into_iter().next().map(|symbol| {
|
||||||
|
String::from_utf8_lossy(symbol.data()).into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "decode-backend-rqrr")]
|
||||||
|
mod rqrr {
|
||||||
|
use super::{ImageBuffer, Luma, Scanner};
|
||||||
|
|
||||||
|
pub struct Rqrr;
|
||||||
|
|
||||||
|
impl Scanner for Rqrr {
|
||||||
|
fn scan_image(
|
||||||
|
&mut self,
|
||||||
|
image: ImageBuffer<Luma<u8>, Vec<u8>>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let mut image = rqrr::PreparedImage::prepare(image);
|
||||||
|
for grid in image.detect_grids() {
|
||||||
|
if let Ok((_, content)) = grid.decode() {
|
||||||
|
return Some(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn dbg_elapsed(count: u64, instant: Instant) {
|
||||||
|
let elapsed = instant.elapsed().as_secs();
|
||||||
|
let framerate = count as f64 / elapsed as f64;
|
||||||
|
eprintln!("framerate: {count}/{elapsed} = {framerate}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Continuously scan the `index`-th camera for a QR code.
|
/// Continuously scan the `index`-th camera for a QR code.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// The function may return an error if the hardware is unable to scan video or if an image could
|
/// The function may return an error if the hardware is unable to scan video or if an image could
|
||||||
/// not be decoded.
|
/// not be decoded.
|
||||||
#[cfg(feature = "decode-backend-rqrr")]
|
|
||||||
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||||
let device = Device::new(index)?;
|
let device = Device::new(index)?;
|
||||||
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
let mut fmt = device
|
||||||
|
.format()
|
||||||
|
.unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
||||||
fmt.fourcc = FourCC::new(b"MPG1");
|
fmt.fourcc = FourCC::new(b"MPG1");
|
||||||
device.set_format(&fmt)?;
|
device.set_format(&fmt)?;
|
||||||
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature = "decode-backend-zbar")] {
|
||||||
|
let mut scanner = zbar::Zbar::default();
|
||||||
|
} else if #[cfg(feature = "decode-backend-rqrr")] {
|
||||||
|
let mut scanner = rqrr::Rqrr;
|
||||||
|
} else {
|
||||||
|
unimplemented!("neither decode-backend-zbar nor decode-backend-rqrr were selected")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
while Instant::now()
|
#[allow(unused)]
|
||||||
.duration_since(start)
|
let mut count = 0;
|
||||||
< timeout
|
|
||||||
{
|
while Instant::now().duration_since(start) < timeout {
|
||||||
|
count += 1;
|
||||||
let (buffer, _) = stream.next()?;
|
let (buffer, _) = stream.next()?;
|
||||||
let image = ImageReader::new(Cursor::new(buffer))
|
let image = ImageReader::new(Cursor::new(buffer))
|
||||||
.with_guessed_format()?
|
.with_guessed_format()?
|
||||||
.decode()?
|
.decode()?
|
||||||
.to_luma8();
|
.to_luma8();
|
||||||
let mut image = rqrr::PreparedImage::prepare(image);
|
if let Some(content) = scanner.scan_image(image) {
|
||||||
for grid in image.detect_grids() {
|
// dbg_elapsed(count, start);
|
||||||
if let Ok((_, content)) = grid.decode() {
|
return Ok(Some(content));
|
||||||
return Ok(Some(content))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
// dbg_elapsed(count, start);
|
||||||
}
|
|
||||||
|
|
||||||
/// 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));
|
|
||||||
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 mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
|
|
||||||
|
|
||||||
while Instant::now()
|
|
||||||
.duration_since(start)
|
|
||||||
< timeout
|
|
||||||
{
|
|
||||||
let (buffer, _) = stream.next()?;
|
|
||||||
let image = ImageReader::new(Cursor::new(buffer))
|
|
||||||
.with_guessed_format()?
|
|
||||||
.decode()?;
|
|
||||||
let image = keyfork_zbar::image::Image::from(image);
|
|
||||||
for symbol in scanner.scan_image(&image) {
|
|
||||||
return Ok(Some(String::from_utf8_lossy(symbol.data()).to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-zbar"
|
name = "keyfork-zbar"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
repository = "https://git.distrust.co/public/keyfork"
|
repository = "https://git.distrust.co/public/keyfork"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
@ -58,7 +58,7 @@ impl Image {
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
mod impls {
|
mod impls {
|
||||||
use super::*;
|
use super::*;
|
||||||
use image::{DynamicImage, GenericImageView};
|
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma};
|
||||||
|
|
||||||
impl From<DynamicImage> for Image {
|
impl From<DynamicImage> for Image {
|
||||||
fn from(value: DynamicImage) -> Self {
|
fn from(value: DynamicImage) -> Self {
|
||||||
|
@ -70,6 +70,17 @@ mod impls {
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ImageBuffer<Luma<u8>, Vec<u8>>> for Image {
|
||||||
|
fn from(value: ImageBuffer<Luma<u8>, Vec<u8>>) -> Self {
|
||||||
|
let mut image = Self::alloc();
|
||||||
|
let (width, height) = value.dimensions();
|
||||||
|
image.set_size(width, height);
|
||||||
|
image.set_format(b"Y800");
|
||||||
|
image.set_data(value.into_raw());
|
||||||
|
image
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Image {
|
impl Drop for Image {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "keyfork-tests"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
assert_cmd = "2.0.16"
|
||||||
|
keyforkd = { workspace = true, features = ["default"] }
|
||||||
|
sequoia-openpgp = { workspace = true, features = ["crypto-nettle"] }
|
|
@ -0,0 +1 @@
|
||||||
|
mod openpgp;
|
|
@ -0,0 +1,58 @@
|
||||||
|
use sequoia_openpgp as openpgp;
|
||||||
|
|
||||||
|
use assert_cmd::Command;
|
||||||
|
use openpgp::{
|
||||||
|
parse::{PacketParser, Parse},
|
||||||
|
policy::StandardPolicy,
|
||||||
|
types::KeyFlags,
|
||||||
|
Cert,
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEYFORK_BIN: &str = "keyfork";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
let policy = StandardPolicy::new();
|
||||||
|
|
||||||
|
let command_output = Command::cargo_bin(KEYFORK_BIN)
|
||||||
|
.unwrap()
|
||||||
|
.args([
|
||||||
|
"derive",
|
||||||
|
"openpgp",
|
||||||
|
"Ryan Heywood (RyanSquared) <ryan@distrust.co>",
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
let packets = PacketParser::from_bytes(&command_output.get_output().stdout).unwrap();
|
||||||
|
let cert = Cert::try_from(packets).unwrap();
|
||||||
|
|
||||||
|
// assert the cert contains _any_ secret key data
|
||||||
|
assert!(
|
||||||
|
cert.is_tsk(),
|
||||||
|
"exported key should contain secret key data, indicated by the key being a TSK"
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert the correct keys were added in the correct order
|
||||||
|
let mut key_formats = std::collections::HashSet::from([
|
||||||
|
KeyFlags::empty().set_certification(),
|
||||||
|
KeyFlags::empty().set_signing(),
|
||||||
|
KeyFlags::empty()
|
||||||
|
.set_transport_encryption()
|
||||||
|
.set_storage_encryption(),
|
||||||
|
KeyFlags::empty().set_authentication(),
|
||||||
|
]);
|
||||||
|
let valid_cert = cert.with_policy(&policy, None).unwrap();
|
||||||
|
for key in valid_cert.keys() {
|
||||||
|
let flags = key.key_flags().unwrap();
|
||||||
|
assert!(
|
||||||
|
key_formats.remove(&flags),
|
||||||
|
"could not find key flag set: {flags:?}"
|
||||||
|
);
|
||||||
|
key.alive().expect("is live after being generated");
|
||||||
|
key.parts_into_secret().expect("has secret keys");
|
||||||
|
}
|
||||||
|
if !key_formats.is_empty() {
|
||||||
|
panic!("remaining key formats: {key_formats:?}");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
mod derive;
|
|
@ -0,0 +1,2 @@
|
||||||
|
#[cfg(test)]
|
||||||
|
mod keyfork;
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-bug"
|
name = "keyfork-bug"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,12 @@
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ```rust,should_panic
|
//! ```rust,should_panic
|
||||||
|
//! let rows = 24;
|
||||||
|
//! let input_lines_len = 25;
|
||||||
|
//! assert!(input_lines_len < rows, "{input_lines_len} can't fit in {rows} lines!");
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ```rust,should_panic
|
||||||
//! use std::fs::File;
|
//! use std::fs::File;
|
||||||
//! use keyfork_bug as bug;
|
//! use keyfork_bug as bug;
|
||||||
//!
|
//!
|
||||||
|
@ -83,6 +89,29 @@ macro_rules! bug {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assert a condition is true, otherwise throwing an error using Keyfork Bug.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let expectations = "conceivable!";
|
||||||
|
/// let circumstances = "otherwise";
|
||||||
|
/// assert!(circumstances != expectations, "you keep using that word...");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Variables can be used in the error message, without having to pass them manually.
|
||||||
|
///
|
||||||
|
/// ```rust,should_panic
|
||||||
|
/// let rows = 24;
|
||||||
|
/// let input_lines_len = 25;
|
||||||
|
/// assert!(input_lines_len < rows, "{input_lines_len} can't fit in {rows} lines!");
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! assert {
|
||||||
|
($cond:expr, $($input:tt)*) => {
|
||||||
|
std::assert!($cond, "{}", keyfork_bug::bug!($($input)*));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return a closure that, when called, panics with a bug report message for Keyfork. Returning a
|
/// 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
|
/// 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.
|
/// argument, so it is suitable for being used with [`Result`] types instead of [`Option`] types.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyfork-prompt"
|
name = "keyfork-prompt"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
description = "Prompt management utilities for Keyfork"
|
description = "Prompt management utilities for Keyfork"
|
||||||
repository = "https://git.distrust.co/public/keyfork"
|
repository = "https://git.distrust.co/public/keyfork"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
@ -1,26 +1,47 @@
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::default_handler;
|
||||||
prompt_validated_wordlist,
|
|
||||||
validators::{mnemonic, Validator},
|
|
||||||
default_handler,
|
|
||||||
};
|
|
||||||
|
|
||||||
use keyfork_mnemonic::English;
|
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||||
|
pub enum Choices {
|
||||||
|
Retry,
|
||||||
|
Continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Choices {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Choices::Retry => write!(
|
||||||
|
f,
|
||||||
|
"Retry with some really long text that I want to cause issues with."
|
||||||
|
),
|
||||||
|
Choices::Continue => write!(
|
||||||
|
f,
|
||||||
|
"Continue with some really long text that I want to cause issues with."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl keyfork_prompt::Choice for Choices {
|
||||||
|
fn identifier(&self) -> Option<char> {
|
||||||
|
Some(match self {
|
||||||
|
Choices::Retry => 'r',
|
||||||
|
Choices::Continue => 'c',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut handler = default_handler().unwrap();
|
let mut handler = default_handler()?;
|
||||||
let transport_validator = mnemonic::MnemonicSetValidator {
|
|
||||||
word_lengths: [24],
|
|
||||||
};
|
|
||||||
|
|
||||||
let mnemonics = prompt_validated_wordlist::<English, _>(
|
let choice = keyfork_prompt::prompt_choice(
|
||||||
&mut *handler,
|
&mut *handler,
|
||||||
"Enter a 24-word mnemonic: ",
|
"Here are some options!",
|
||||||
3,
|
&[Choices::Retry, Choices::Continue],
|
||||||
&*transport_validator.to_fn(),
|
);
|
||||||
)?;
|
|
||||||
assert_eq!(mnemonics[0].as_bytes().len(), 32);
|
dbg!(&choice);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,12 @@
|
||||||
//! directly intended to be machine-readable, but can be used for scriptable automation in a
|
//! directly intended to be machine-readable, but can be used for scriptable automation in a
|
||||||
//! fashion similar to a terminal handler.
|
//! fashion similar to a terminal handler.
|
||||||
|
|
||||||
use std::io::{IsTerminal, Write};
|
use std::{
|
||||||
|
io::{IsTerminal, Write},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{BoxResult, Error, Message, PromptHandler, Result};
|
use crate::{BoxResult, Choice, Error, Message, PromptHandler, Result};
|
||||||
|
|
||||||
/// A headless prompt handler, usable in situations when a terminal might not be available, or for
|
/// A headless prompt handler, usable in situations when a terminal might not be available, or for
|
||||||
/// scripting purposes where manual input from a terminal is not desirable.
|
/// scripting purposes where manual input from a terminal is not desirable.
|
||||||
|
@ -58,17 +61,47 @@ impl PromptHandler for Headless {
|
||||||
fn prompt_message(&mut self, prompt: Message) -> Result<()> {
|
fn prompt_message(&mut self, prompt: Message) -> Result<()> {
|
||||||
match prompt {
|
match prompt {
|
||||||
Message::Text(s) => {
|
Message::Text(s) => {
|
||||||
self.stderr.write_all(s.as_bytes())?;
|
writeln!(&mut self.stderr, "{s}")?;
|
||||||
self.stderr.flush()?;
|
self.stderr.flush()?;
|
||||||
}
|
}
|
||||||
Message::Data(s) => {
|
Message::Data(s) => {
|
||||||
self.stderr.write_all(s.as_bytes())?;
|
writeln!(&mut self.stderr, "{s}")?;
|
||||||
self.stderr.flush()?;
|
self.stderr.flush()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
writeln!(&mut self.stderr, "Press enter to continue.")?;
|
||||||
|
self.stdin.read_line(&mut String::new())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_choice_num(&mut self, prompt: &str, choices: &[Box<dyn Choice>]) -> Result<usize> {
|
||||||
|
writeln!(&mut self.stderr, "{prompt}")?;
|
||||||
|
for (i, choice) in choices.iter().enumerate() {
|
||||||
|
match choice.identifier() {
|
||||||
|
Some(identifier) => {
|
||||||
|
writeln!(&mut self.stderr, "{i}. ({identifier})\t{choice}")?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
writeln!(&mut self.stderr, "{i}.\t{choice}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.stderr.flush()?;
|
||||||
|
let mut line = String::new();
|
||||||
|
self.stdin.read_line(&mut line)?;
|
||||||
|
let selector_char = line.chars().next();
|
||||||
|
if let Some(selector @ ('a'..='z' | 'A'..='Z')) = selector_char {
|
||||||
|
if let Some((index, _)) = choices.iter().enumerate().find(|(_, choice)| {
|
||||||
|
choice
|
||||||
|
.identifier()
|
||||||
|
.is_some_and(|identifier| selector == identifier)
|
||||||
|
}) {
|
||||||
|
return Ok(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usize::from_str(line.trim()).map_err(|e| Error::Custom(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
fn prompt_validated_wordlist(
|
fn prompt_validated_wordlist(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
|
@ -85,7 +118,7 @@ impl PromptHandler for Headless {
|
||||||
self.stdin.read_line(&mut line)?;
|
self.stdin.read_line(&mut line)?;
|
||||||
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
||||||
last_error = e.to_string();
|
last_error = e.to_string();
|
||||||
self.stderr.write_all(e.to_string().as_bytes())?;
|
writeln!(&mut self.stderr, "{e}")?;
|
||||||
self.stderr.flush()?;
|
self.stderr.flush()?;
|
||||||
} else {
|
} else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -108,8 +141,7 @@ impl PromptHandler for Headless {
|
||||||
self.stdin.read_line(&mut line)?;
|
self.stdin.read_line(&mut line)?;
|
||||||
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
||||||
last_error = e.to_string();
|
last_error = e.to_string();
|
||||||
self.stderr.write_all(e.to_string().as_bytes())?;
|
writeln!(&mut self.stderr, "{e}")?;
|
||||||
self.stderr.write_all(b"\n")?;
|
|
||||||
self.stderr.flush()?;
|
self.stderr.flush()?;
|
||||||
} else {
|
} else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
|
@ -50,6 +50,10 @@ pub enum Error {
|
||||||
/// An error occurred while interacting with a terminal.
|
/// An error occurred while interacting with a terminal.
|
||||||
#[error("IO Error: {0}")]
|
#[error("IO Error: {0}")]
|
||||||
IO(#[from] std::io::Error),
|
IO(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// An unexpected error occurred.
|
||||||
|
#[error("{0}")]
|
||||||
|
Custom(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
|
@ -64,6 +68,21 @@ pub enum Message {
|
||||||
Data(String),
|
Data(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A type that may represent an identifier to be used when using a choice prompt.
|
||||||
|
pub trait Choice: std::fmt::Display {
|
||||||
|
/// The identifier for the type.
|
||||||
|
fn identifier(&self) -> Option<char> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this way, we can make Box<dyn T> from &T
|
||||||
|
impl<T: Choice> Choice for &T {
|
||||||
|
fn identifier(&self) -> Option<char> {
|
||||||
|
Choice::identifier(*self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub type BoxResult = std::result::Result<(), Box<dyn std::error::Error>>;
|
pub type BoxResult = std::result::Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
@ -98,6 +117,16 @@ pub trait PromptHandler {
|
||||||
/// occurred while waiting for the user to dismiss the message.
|
/// occurred while waiting for the user to dismiss the message.
|
||||||
fn prompt_message(&mut self, prompt: Message) -> Result<()>;
|
fn prompt_message(&mut self, prompt: Message) -> Result<()>;
|
||||||
|
|
||||||
|
/// Prompt the user for a choice between the provided options. The returned value is the index
|
||||||
|
/// of the given choice.
|
||||||
|
///
|
||||||
|
/// This method SHOULD NOT be used directly. Instead, use [`prompt_choice`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the message was not able to be displayed or if the input
|
||||||
|
/// could not be read.
|
||||||
|
fn prompt_choice_num(&mut self, prompt: &str, choices: &[Box<dyn Choice>]) -> Result<usize>;
|
||||||
|
|
||||||
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
/// 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
|
/// 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.
|
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
||||||
|
@ -133,6 +162,29 @@ pub trait PromptHandler {
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prompt the user for a choice between the provided options. The returned value is the selected
|
||||||
|
/// choice.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the message was not able to be displayed or if the input
|
||||||
|
/// could not be read.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
|
pub fn prompt_choice<T>(
|
||||||
|
handler: &mut dyn PromptHandler,
|
||||||
|
prompt: &str,
|
||||||
|
choices: &'static [T],
|
||||||
|
) -> Result<T>
|
||||||
|
where
|
||||||
|
T: Choice + Copy + 'static,
|
||||||
|
{
|
||||||
|
let boxed_choices = choices
|
||||||
|
.iter()
|
||||||
|
.map(|c| Box::new(c) as Box<dyn Choice>)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let choice = handler.prompt_choice_num(prompt, boxed_choices.as_slice())?;
|
||||||
|
Ok(choices[choice])
|
||||||
|
}
|
||||||
|
|
||||||
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
/// 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
|
/// 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.
|
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
||||||
|
@ -217,9 +269,9 @@ pub fn default_handler() -> Result<Box<dyn PromptHandler>, DefaultHandlerError>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// stdout can be not-a-terminal and we'll just override it anyways, stdin is the
|
// we can revert stdin to a readable input by using raw mode, but we can't do the more
|
||||||
// important one.
|
// significant operations if we don't have access to a terminal stderr
|
||||||
if std::io::stdin().is_terminal() {
|
if std::io::stderr().is_terminal() {
|
||||||
// because this is a "guessed" handler, let's take the nice route and not error, just skip.
|
// because this is a "guessed" handler, let's take the nice route and not error, just skip.
|
||||||
if let Ok(terminal) = default_terminal() {
|
if let Ok(terminal) = default_terminal() {
|
||||||
return Ok(Box::new(terminal));
|
return Ok(Box::new(terminal));
|
||||||
|
|
|
@ -21,7 +21,7 @@ use keyfork_crossterm::{
|
||||||
|
|
||||||
use keyfork_bug::bug;
|
use keyfork_bug::bug;
|
||||||
|
|
||||||
use crate::{BoxResult, Error, Message, PromptHandler};
|
use crate::{BoxResult, Choice, Error, Message, PromptHandler};
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
@ -129,14 +129,26 @@ where
|
||||||
{
|
{
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.write
|
self.write
|
||||||
|
.execute(cursor::EnableBlinking)
|
||||||
|
.expect(bug!("can't enable blinking"))
|
||||||
|
.execute(cursor::Show)
|
||||||
|
.expect(bug!("can't show cursor"))
|
||||||
.execute(DisableBracketedPaste)
|
.execute(DisableBracketedPaste)
|
||||||
.expect(bug!("can't restore bracketed paste"));
|
.expect(bug!("can't restore bracketed paste"));
|
||||||
self.write
|
|
||||||
.execute(LeaveAlternateScreen)
|
|
||||||
.expect(bug!("can't leave alternate screen"));
|
|
||||||
self.terminal
|
self.terminal
|
||||||
.disable_raw_mode()
|
.disable_raw_mode()
|
||||||
.expect(bug!("can't disable raw mode"));
|
.expect(bug!("can't disable raw mode"));
|
||||||
|
// we don't want to clear error messages
|
||||||
|
if !std::thread::panicking() {
|
||||||
|
self.write
|
||||||
|
.queue(LeaveAlternateScreen)
|
||||||
|
.expect(bug!("can't leave alternate screen"))
|
||||||
|
.queue(terminal::Clear(terminal::ClearType::All))
|
||||||
|
.expect(bug!("can't clear screen"))
|
||||||
|
.queue(cursor::MoveTo(0, 0))
|
||||||
|
.expect(bug!("can't move to origin"));
|
||||||
|
}
|
||||||
|
self.write.flush().expect(bug!("can't execute terminal reset commands"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,24 +190,178 @@ where
|
||||||
W: Write + AsRawFd + Sized,
|
W: Write + AsRawFd + Sized,
|
||||||
{
|
{
|
||||||
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
|
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
|
||||||
let mut terminal = self.lock().alternate_screen()?;
|
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
|
||||||
terminal
|
terminal
|
||||||
.queue(terminal::Clear(terminal::ClearType::All))?
|
.queue(terminal::Clear(terminal::ClearType::All))?
|
||||||
.queue(cursor::MoveTo(0, 0))?;
|
.queue(cursor::MoveTo(0, 0))?;
|
||||||
let mut lines = prompt.lines().peekable();
|
let mut lines = prompt.lines().peekable();
|
||||||
|
let mut prefix_length = 0;
|
||||||
while let Some(line) = lines.next() {
|
while let Some(line) = lines.next() {
|
||||||
|
prefix_length = line.len();
|
||||||
terminal.queue(Print(line))?;
|
terminal.queue(Print(line))?;
|
||||||
if lines.peek().is_some() {
|
if lines.peek().is_some() {
|
||||||
terminal
|
terminal.queue(cursor::MoveToNextLine(1))?;
|
||||||
.queue(cursor::MoveDown(1))?
|
|
||||||
.queue(cursor::MoveToColumn(0))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.flush()?;
|
terminal.flush()?;
|
||||||
|
|
||||||
let mut line = String::new();
|
let (mut cols, mut _rows) = terminal.size()?;
|
||||||
terminal.read.read_line(&mut line)?;
|
|
||||||
Ok(line)
|
let mut input = String::new();
|
||||||
|
loop {
|
||||||
|
let input_len = input.len();
|
||||||
|
match read()? {
|
||||||
|
Event::Resize(new_cols, new_rows) => {
|
||||||
|
cols = new_cols;
|
||||||
|
_rows = new_rows;
|
||||||
|
}
|
||||||
|
Event::Key(k) => match k.code {
|
||||||
|
KeyCode::Enter => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
if input.pop().is_some() && prefix_length + input_len < cols as usize {
|
||||||
|
terminal
|
||||||
|
.queue(cursor::MoveLeft(1))?
|
||||||
|
.queue(Print(" "))?
|
||||||
|
.queue(cursor::MoveLeft(1))?
|
||||||
|
.flush()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('w') if k.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
|
let mut has_deleted_text = true;
|
||||||
|
while input.pop().is_some_and(char::is_whitespace) {
|
||||||
|
has_deleted_text = false;
|
||||||
|
}
|
||||||
|
while input.pop().is_some_and(|c| !c.is_whitespace()) {
|
||||||
|
has_deleted_text = true;
|
||||||
|
}
|
||||||
|
if !input.is_empty() && has_deleted_text {
|
||||||
|
input.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
|
return Err(Error::CtrlC);
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
input.push(c);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
let printable_start = if prefix_length + input.len() < cols as usize {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
let printable_space = (cols as usize) - prefix_length;
|
||||||
|
input.len() - (printable_space - 1)
|
||||||
|
};
|
||||||
|
terminal
|
||||||
|
.queue(cursor::MoveToColumn(prefix_length as u16))?
|
||||||
|
.queue(terminal::Clear(terminal::ClearType::UntilNewLine))?
|
||||||
|
.queue(Print(&input[printable_start..]))?
|
||||||
|
.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_choice_num(&mut self, prompt: &str, choices: &[Box<dyn Choice>]) -> Result<usize> {
|
||||||
|
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
|
||||||
|
terminal
|
||||||
|
.queue(terminal::Clear(terminal::ClearType::All))?
|
||||||
|
.queue(cursor::MoveTo(0, 0))?
|
||||||
|
.queue(cursor::Hide)?;
|
||||||
|
|
||||||
|
for line in prompt.lines() {
|
||||||
|
terminal
|
||||||
|
.queue(Print(line))?
|
||||||
|
.queue(cursor::MoveToNextLine(1))?;
|
||||||
|
|
||||||
|
terminal.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut active_choice = 0;
|
||||||
|
let mut drawn = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (cols, rows) = terminal.size()?;
|
||||||
|
// all choices, plus their padding, plus the spacing between, minus whitespace at end.
|
||||||
|
let max_size = choices
|
||||||
|
.iter()
|
||||||
|
.fold(0usize, |agg, choice| agg + choice.to_string().len() + 2)
|
||||||
|
+ std::cmp::max(choices.len(), 1)
|
||||||
|
- 1;
|
||||||
|
let horizontal = max_size < cols.into();
|
||||||
|
keyfork_bug::assert!(
|
||||||
|
horizontal || usize::from(rows) > prompt.lines().count() + choices.len(),
|
||||||
|
"screen too small, can't fit choices on {rows}x{cols}",
|
||||||
|
);
|
||||||
|
if horizontal {
|
||||||
|
terminal.queue(cursor::MoveToColumn(0))?;
|
||||||
|
} else if drawn {
|
||||||
|
terminal
|
||||||
|
.queue(cursor::MoveUp(
|
||||||
|
choices
|
||||||
|
.len()
|
||||||
|
.saturating_sub(1)
|
||||||
|
.try_into()
|
||||||
|
.expect(keyfork_bug::bug!("more than {} choices provided", u16::MAX)),
|
||||||
|
))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?;
|
||||||
|
} else {
|
||||||
|
drawn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut iter = choices.iter().enumerate().peekable();
|
||||||
|
while let Some((i, choice)) = iter.next() {
|
||||||
|
// if active choice, flip foreground and background
|
||||||
|
// if active choice, wrap in []
|
||||||
|
// if not, wrap in spaces, to preserve spacing and prevent redraws
|
||||||
|
if i == active_choice {
|
||||||
|
terminal.queue(PrintStyledContent(Stylize::reverse(format!("[{choice}]"))))?;
|
||||||
|
} else {
|
||||||
|
terminal.queue(Print(format!(" {choice} ")))?;
|
||||||
|
}
|
||||||
|
if iter.peek().is_some() {
|
||||||
|
if horizontal {
|
||||||
|
terminal.queue(Print(" "))?;
|
||||||
|
} else {
|
||||||
|
terminal.queue(cursor::MoveToNextLine(1))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminal.flush()?;
|
||||||
|
|
||||||
|
if let Event::Key(k) = read()? {
|
||||||
|
match k.code {
|
||||||
|
KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
|
return Err(Error::CtrlC);
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
for (i, choice) in choices.iter().enumerate() {
|
||||||
|
if choice.identifier().is_some_and(|id| id == c) {
|
||||||
|
active_choice = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Left | KeyCode::Up => {
|
||||||
|
active_choice = active_choice.saturating_sub(1);
|
||||||
|
}
|
||||||
|
KeyCode::Right | KeyCode::Down => match choices.len().saturating_sub(active_choice) {
|
||||||
|
0 | 1 => {}
|
||||||
|
_ => {
|
||||||
|
active_choice += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
KeyCode::Enter => {
|
||||||
|
return Ok(active_choice);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_validated_wordlist(
|
fn prompt_validated_wordlist(
|
||||||
|
@ -241,9 +407,7 @@ where
|
||||||
prefix_length = line.len();
|
prefix_length = line.len();
|
||||||
terminal.queue(Print(line))?;
|
terminal.queue(Print(line))?;
|
||||||
if lines.peek().is_some() {
|
if lines.peek().is_some() {
|
||||||
terminal
|
terminal.queue(cursor::MoveToNextLine(1))?;
|
||||||
.queue(cursor::MoveDown(1))?
|
|
||||||
.queue(cursor::MoveToColumn(0))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.flush()?;
|
terminal.flush()?;
|
||||||
|
@ -402,9 +566,7 @@ where
|
||||||
prefix_length = line.len();
|
prefix_length = line.len();
|
||||||
terminal.queue(Print(line))?;
|
terminal.queue(Print(line))?;
|
||||||
if lines.peek().is_some() {
|
if lines.peek().is_some() {
|
||||||
terminal
|
terminal.queue(cursor::MoveToNextLine(1))?;
|
||||||
.queue(cursor::MoveDown(1))?
|
|
||||||
.queue(cursor::MoveToColumn(0))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.flush()?;
|
terminal.flush()?;
|
||||||
|
@ -470,21 +632,17 @@ where
|
||||||
let len = std::cmp::min(u16::MAX as usize, word.len()) as u16;
|
let len = std::cmp::min(u16::MAX as usize, word.len()) as u16;
|
||||||
written_chars += len + 1;
|
written_chars += len + 1;
|
||||||
if written_chars > cols {
|
if written_chars > cols {
|
||||||
terminal
|
terminal.queue(cursor::MoveToNextLine(1))?;
|
||||||
.queue(cursor::MoveDown(1))?
|
|
||||||
.queue(cursor::MoveToColumn(0))?;
|
|
||||||
written_chars = len + 1;
|
written_chars = len + 1;
|
||||||
}
|
}
|
||||||
terminal.queue(Print(word))?.queue(Print(" "))?;
|
terminal.queue(Print(word))?.queue(Print(" "))?;
|
||||||
}
|
}
|
||||||
terminal
|
terminal.queue(cursor::MoveToNextLine(1))?;
|
||||||
.queue(cursor::MoveDown(1))?
|
|
||||||
.queue(cursor::MoveToColumn(0))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Data(data) => {
|
Message::Data(data) => {
|
||||||
let count = data.lines().count();
|
let count = data.lines().count();
|
||||||
// NOTE: GE to allow a MoveDown(1)
|
// NOTE: GE to allow a MoveToNextLine(1)
|
||||||
if count >= rows as usize {
|
if count >= rows as usize {
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"{} {count} {} {rows} {}",
|
"{} {count} {} {rows} {}",
|
||||||
|
@ -492,14 +650,12 @@ where
|
||||||
);
|
);
|
||||||
terminal
|
terminal
|
||||||
.queue(Print(msg))?
|
.queue(Print(msg))?
|
||||||
.queue(cursor::MoveDown(1))?
|
.queue(cursor::MoveToNextLine(1))?;
|
||||||
.queue(cursor::MoveToColumn(0))?;
|
|
||||||
} else {
|
} else {
|
||||||
for line in data.lines() {
|
for line in data.lines() {
|
||||||
terminal
|
terminal
|
||||||
.queue(Print(line))?
|
.queue(Print(line))?
|
||||||
.queue(cursor::MoveDown(1))?
|
.queue(cursor::MoveToNextLine(1))?;
|
||||||
.queue(cursor::MoveToColumn(0))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -521,7 +677,6 @@ where
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.queue(cursor::EnableBlinking)?.flush()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
scripts_dir="$(dirname $0)"
|
scripts_dir="$(dirname "$0")"
|
||||||
python_script="$scripts_dir/generate-dependency-queue.py"
|
python_script="$scripts_dir/generate-dependency-queue.py"
|
||||||
registry_url="https://git.distrust.co/api/packages/public/cargo"
|
registry_url="https://git.distrust.co/api/packages/public/cargo"
|
||||||
search_url="${registry_url}/api/v1/crates"
|
search_url="${registry_url}/api/v1/crates"
|
||||||
|
|
||||||
cargo metadata --format-version=1 | python3 "$python_script" | while read crate version; do
|
cargo metadata --format-version=1 | python3 "$python_script" | while read -r crate version; do
|
||||||
# Verify the package does not exist
|
# Verify the package does not exist
|
||||||
|
if test "${crate}" = "keyfork-tests"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
if ! curl "${search_url}?q=${crate}" 2>/dev/null | jq -e "$(printf '.crates | .[] | select(.name == "%s" and .max_version == "%s")' "$crate" "$version")" >/dev/null; then
|
if ! curl "${search_url}?q=${crate}" 2>/dev/null | jq -e "$(printf '.crates | .[] | select(.name == "%s" and .max_version == "%s")' "$crate" "$version")" >/dev/null; then
|
||||||
cargo publish --registry distrust -p "$crate"
|
cargo publish --registry distrust -p "$crate"
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
set -eu
|
set -eu
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
|
@ -8,8 +9,8 @@ temp_file="$(mktemp)"
|
||||||
|
|
||||||
cargo metadata --format-version=1 | jq -r '.packages[] | select(.source == null) | .name + " " + .manifest_path + " " + .version' > "$temp_file"
|
cargo metadata --format-version=1 | jq -r '.packages[] | select(.source == null) | .name + " " + .manifest_path + " " + .version' > "$temp_file"
|
||||||
|
|
||||||
while read crate manifest_path version <&3; do
|
while read -r crate manifest_path version <&3; do
|
||||||
crate_path="$(dirname $manifest_path)"
|
crate_path="$(dirname "$manifest_path")"
|
||||||
git_log="$(git log --format='%h %s' "$LAST_REF".."$CURRENT_REF" "$crate_path")"
|
git_log="$(git log --format='%h %s' "$LAST_REF".."$CURRENT_REF" "$crate_path")"
|
||||||
git_tag="$(git tag --list "$crate-v${version}")"
|
git_tag="$(git tag --list "$crate-v${version}")"
|
||||||
if test ! -z "$git_log" -a -z "$git_tag"; then
|
if test ! -z "$git_log" -a -z "$git_tag"; then
|
||||||
|
@ -22,6 +23,7 @@ while read crate manifest_path version <&3; do
|
||||||
echo ""
|
echo ""
|
||||||
echo "# Crate: ${crate} ${version}"
|
echo "# Crate: ${crate} ${version}"
|
||||||
} | git tag --sign "${crate}-v${version}" -F - -e
|
} | git tag --sign "${crate}-v${version}" -F - -e
|
||||||
|
reset
|
||||||
echo "Making new tag: ${crate}-v${version}"
|
echo "Making new tag: ${crate}-v${version}"
|
||||||
fi
|
fi
|
||||||
done 3<"$temp_file"
|
done 3<"$temp_file"
|
||||||
|
|
Loading…
Reference in New Issue