Compare commits

...

119 Commits

Author SHA1 Message Date
Ryan Heywood 4714f616ea
Makefile: don't use backticks 2025-05-24 21:08:55 -04:00
Ryan Heywood 71bd819797
minor version bumps 2025-05-24 20:43:32 -04:00
Ryan Heywood fc930399de
remove keyfork-crossterm 2025-05-24 20:22:35 -04:00
Ryan Heywood e7a776f59f
all crates: make pedantic clippy happy 2025-05-17 19:36:11 -04:00
Ryan Heywood 672cc6a699
deny.toml: ignore unmaintained openpgp-card-sequoia 2025-05-17 17:04:22 -04:00
Ryan Heywood df552250ba
keyfork-zbar-sys: dedup bindgen 2025-05-17 17:01:26 -04:00
Ryan Heywood 0cb96782ef
all crates: `cargo fmt` 2025-05-17 16:02:34 -04:00
Ryan Heywood 625e8e490b
all crates: update code to make clippy::all happy 2025-05-17 15:53:38 -04:00
Ryan Heywood c6e274c4da
keyfork-qrcode: remove debug printings 2025-05-10 16:17:20 -04:00
Ryan Heywood 18773d351c
keyfork-qrcode: add framerate tracker 2025-05-10 15:01:22 -04:00
Ryan Heywood 2083eb216f
keyfork-qrcode: handle two good scans 2025-05-10 14:42:50 -04:00
Ryan Heywood f63b686e70
keyfork-qrcode: wait while empty AND running 2025-05-10 14:25:55 -04:00
Ryan Heywood 0737ca6907
keyfork-qrcode: add debug printing 2025-05-10 14:03:45 -04:00
Ryan Heywood acdf894558
keyfork-qrcode: add threaded handler 2025-05-10 13:42:07 -04:00
Ryan Heywood 9b2a8a5967
Keyfork v0.3.3 2025-04-18 17:45:43 -04:00
Ryan Heywood e7be91bdd4
keyfork-{shard,prompt}: add Yes/No prompt for verifying QR codes 2025-04-18 17:12:28 -04:00
Ryan Heywood 739921d915
WIP: add checksum to shard 2025-04-15 18:35:13 -04:00
Ryan Heywood 64c75085f4
add derivation path for Shard keys 2025-04-08 15:24:58 -04:00
Ryan Heywood 00e35bcb7d
Keyfork v0.3.1 2025-03-25 22:25:36 -04:00
Ryan Heywood d0019a93f0
keyfork-shard: break loop when receiving valid QR code 2025-03-25 20:15:40 -04:00
Ryan Heywood 020fa4d25e
keyfork: remove sneaky space in Cargo.toml 2025-02-25 23:33:47 -05:00
Ryan Heywood 76ca4b0812
Release keyfork v0.3.0 2025-02-25 23:23:39 -05:00
Ryan Heywood 53665cac2e
keyfork: the wizard is dead! long live the mnemonic generator! 2025-02-25 23:00:23 -05:00
Ryan Heywood a1c3d52c14
keyfork: restructure wizard shard key generation
also: `keyfork provision shard`
2025-02-25 17:02:35 -05:00
Ryan Heywood 674e2e93c5
keyfork: restructure CLI commands to act more like the other commands 2025-02-24 23:16:27 -05:00
Ryan Heywood 88a05f23ac
keyfork-prompt: add choice mechanism, & add to keyfork-shard 2025-02-22 05:29:49 -05:00
Ryan Heywood 98b9dbb811
keyfork-qrcode: restructure to prefer libzbar and compile with both enabled 2025-02-22 02:48:13 -05:00
Ryan Heywood 723194fdd7
keyfork mnemonic generate: userid equivalency, rename provisioner cert_output to output 2025-02-19 20:35:29 -05:00
Ryan Heywood db19b30bfe
keyfork mnemonic generate: feedback improvements
* Touch policy is now set to `on` by default (not fixed, as that's
  irreversible).
* The value passed to `--encrypt-to-self` is the actual encrypted
  output.
* The `cert_output` passed to `--encrypt-to-self` by default is the
  fingerprint of the certificate.
* The OpenPGP provisioner can now be used without identifiers, if the
  correct amount of smartcards are actively plugged into the current
  system.
* The OpenPGP provisioner, when run without `--encrypt-to-self`, will
  output the OpenPGP certificate for the smartcard.
2025-02-19 20:12:27 -05:00
Ryan Heywood 0243212c80
keyfork-prompt: clear terminal before leaving alt screen; fixes linux terminal 2025-02-19 16:31:06 -05:00
Ryan Heywood 083eb16b39
scripts: minor improvements 2025-02-04 22:01:56 -05:00
Ryan Heywood aa8526cda0
Release keyfork-shard v0.3.1 2025-02-04 22:01:08 -05:00
Ryan Heywood e1c3e38fc7
Release keyfork v0.2.6 2025-02-04 21:45:45 -05:00
Ryan Heywood 4e342ac7a9
keyfork: add `--daemon` 2025-02-04 21:32:14 -05:00
Ryan Heywood 35e0eb57a0
keyfork-prompt: use raw mode for input 2025-01-30 12:10:36 -05:00
Ryan Heywood c232828290
superpower `keyfork mnemonic generate` 2025-01-27 11:59:44 -05:00
Ryan Heywood 8756c3d233
keyfork wizard generate-shard-secret: allow exporting certificates and cross-sign generated keys 2025-01-24 08:06:40 -05:00
Ryan Heywood c95ed0b729
keyfork shard metadata: initial commit 2025-01-24 08:02:30 -05:00
Ryan Heywood 19fbb51d12
keyfork-tests: initial commit. also, fixup test_util's Panicable to not be generic. it's always unit type 2025-01-16 04:20:12 -05:00
Ryan Heywood adb5293f1d
keyfork derive openpgp: export secret keys instead of public certs 2025-01-15 16:19:46 -05:00
Ryan Heywood a233686996
Add maintenance document, fix bug that snuck through 2025-01-04 02:12:50 -05:00
Ryan Heywood 7fab63c1ae
Release keyfork v0.2.5 2025-01-04 02:07:28 -05:00
Ryan Heywood 503c6fa0b4
keyfork derive key: initial commit 2025-01-04 01:49:01 -05:00
Ryan Heywood c46f9e48b7
move things to use default handler mechanism 2025-01-04 01:04:52 -05:00
Ryan Heywood f8db8702ce
keyfork-prompt: add Headless 2025-01-04 00:33:14 -05:00
Ryan Heywood 92dde3dcee
keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers 2025-01-03 23:11:33 -05:00
Ryan Heywood d7bf3d16e1
keyfork-shard: move to blahaj 2024-11-21 17:24:06 -05:00
Ryan Heywood 9e4d5649d9
audits: add audit from NCC and Cure53 following release of `blahaj` 2024-11-21 17:21:23 -05:00
Ryan Heywood 6a3244df01
Cargo.lock: bump g2p, remove syn 1.x 2024-08-14 14:31:03 -04:00
Ryan Heywood be6d562b33
keyfork-qrcode: use image::ImageReader over image::io::Reader (deprecated) 2024-08-14 13:50:48 -04:00
Ryan Heywood 6317cc964f
Cargo.lock: bump deps, dupe generic-array :( 2024-08-12 01:07:43 -04:00
Ryan Heywood 305e070b93
Cargo.lock: bump multiple deps to deduplicate 2024-08-12 00:31:18 -04:00
Ryan Heywood 7e5c7ea8fb
Cargo.lock: bump lalrpop to remove duplicate regex-syntax 2024-08-12 00:11:25 -04:00
Ryan Heywood 63b4677b19
deny.toml: update to not use deprecated keys 2024-08-11 23:48:44 -04:00
Ryan Heywood 1d68dd19d9
fry up some bacon 2024-08-11 23:19:39 -04:00
Ryan Heywood 4ab1e8afa6
add docs to make clippy extra happy 2024-08-11 19:38:18 -04:00
Ryan Heywood a8b2814b17
make clippy happy 2024-08-11 19:25:25 -04:00
Ryan Heywood c36fe0a1b1
keyfork-shard: re-enable standard policy, alive check still disabled, add check for encryption keys when discovering certs 2024-08-11 18:57:43 -04:00
Ryan Heywood c25c11d1a0
release keyfork v0.2.4 2024-08-11 17:33:41 -04:00
Ryan Heywood e1f8ce9a97
cleanup workspace duplicated dependencies 2024-08-11 16:54:06 -04:00
Ryan Heywood b254ba7c56
cleanup post-merge 2024-08-11 14:56:36 -04:00
Ryan Heywood 58d3c34b61
Merge branch 'main' into ryansquared/staging-since-latest 2024-08-11 14:54:02 -04:00
Ryan Heywood ba64db8f00
update Cargo.toml and Cargo.lock 2024-08-08 00:56:40 -04:00
Ryan Heywood dd4354ffc1
keyfork: bump keyfork-shard 2024-08-08 00:53:15 -04:00
Ryan Heywood fa84a2ae5f
keyfork-shard: Be less strict about keys
Rationale: Keyfork Shard runs on Airgap systems. The biggest impact of
using StandardPolicy and checking whether keys are "alive" is the drift
between different Airgap systems where the keys may not be valid at the
same time. Because of this, it is impossible to shard a secret to all
keys at once using a StandardPolicy.

However, we consider these keys to be a trusted input, whether created
by a previous system or generated directly by Keyfork. Because of this,
we can use a NullPolicy to blanketly permit all keys, the same way we
blanketly permit all keys when reconstituting the sharded secret, and
disable the check for whether keys are alive (though, we are still
denying revoked keys).
2024-08-08 00:48:33 -04:00
Ryan Heywood 35f57fcc41
Merge branch 'ryansquared/keyfork-mnemonic-refactors' 2024-08-05 18:00:10 -04:00
Ryan Heywood a2eb5fda11
bump dependencies with listed vulnerabilities (not affected) 2024-08-05 17:48:19 -04:00
Ryan Heywood 5219c5a99f
keyfork: enum-trait-ify choose-your-own commands 2024-08-05 17:43:22 -04:00
Ryan Heywood b26f296a75
keyfork-derive-path-data: move all pathcrafting here 2024-08-01 11:05:46 -04:00
Ryan Heywood 35ab5e65a4
keyfork-mnemonic-util => keyfork-mnemonic 2024-08-01 09:50:30 -04:00
Ryan Heywood 3ee81b6a82
keyfork-mnemonic-util: impl as_slice to_vec into_vec 2024-08-01 09:35:04 -04:00
Ryan Heywood f5627e5bd9
keyfork-mnemonic-util: impl try_from_slice and from_array 2024-08-01 09:29:03 -04:00
Ryan Heywood 02e5b545a4
keyfork-mnemonic-util::generate_seed: return const size array 2024-08-01 09:19:07 -04:00
Ryan Heywood 536e6da5ad
keyforkd{,-client}: lots of documentationings 2024-08-01 08:59:01 -04:00
Ryan Heywood bac762f5be
release keyfork v0.2.2 and keyfork-derive-openpgp 0.1.2 2024-08-01 01:37:18 -04:00
Ryan Heywood c868afedbf
scripts/generate-dependency-queue.py: doc how to run 2024-08-01 01:30:56 -04:00
Ryan Heywood 8d40d2630c
keyfork: add `bottoms-up` wizard 2024-08-01 01:30:54 -04:00
Ryan Heywood 142bea3b9f
keyfork-shard: verify QR code length correctly 2024-05-29 16:16:55 -04:00
Ryan Heywood c65ddbf119
scripts/generate-dependency-queue.py: rewrite 2024-05-16 14:56:31 -04:00
Ryan Heywood d759982853
scripts: add publishing scripts 2024-05-16 02:01:10 -04:00
Ryan Heywood 491d19469a
crates: bump versions 2024-05-16 00:29:28 -04:00
Ryan Heywood 756be9b9d7
Merge remote-tracking branch 'origin/anton/require-min-entropy' 2024-05-05 14:49:12 -04:00
Ryan Heywood ad329131de
Merge remote-tracking branch 'origin/anton/chore/update-readme' 2024-05-05 14:49:01 -04:00
Anton Livaja bcfcc8711f
keyforkd: add warning when loading seed with less than 128 bits 2024-05-05 14:27:10 -04:00
Ryan Heywood de4e98ae07
keyfork-derive-util: black-box checking all zeroes 2024-05-03 23:28:45 -04:00
Ryan Heywood 48ccd7c68f
keyfork-derive-util: add note about potential side-channel when verifying keys 2024-05-03 23:20:53 -04:00
Ryan Heywood d04989ef30
keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct 2024-05-03 23:20:50 -04:00
Ryan Heywood 1a036a0b5f
keyfork-shard: clean up documentation for encrypted shard padding 2024-05-03 22:41:38 -04:00
Anton Livaja fc0350a098
fix: specify OpenPGP 2024-04-29 17:57:05 -04:00
Anton Livaja a18ea7ba0f
chore: make docs regarding factory reset more specific 2024-04-29 13:20:46 -04:00
Ryan Heywood e0687434ef
keyfork-shard: display error message on duplicate key fingerprints found 2024-04-24 13:29:32 -04:00
Ryan Heywood 23db50956f
keyfork-shard: improve wording for counting shardholders 2024-04-24 13:13:48 -04:00
Ryan Heywood 94617722a0
keyfork-shard: ignore duplicate certificate entries 2024-04-22 17:06:13 -04:00
Ryan Heywood 001fc0bccc
remove trailing hitespace :( 2024-04-19 00:30:38 -04:00
Ryan Heywood 6a265ad203
keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes 2024-04-18 23:53:59 -04:00
Ryan Heywood 5d2309e301
keyfork-prompt: add SecurePinValidator for making new, secure, PINs 2024-04-18 23:01:03 -04:00
Ryan Heywood c0b19e2457
keyfork-shard: assert shared secrets are contributory 2024-04-17 15:36:42 -04:00
Ryan Heywood cdf401515f
keyfork wizard: use correct derivation path for re-deriving shard decryption keys 2024-04-17 15:25:22 -04:00
Ryan Heywood f0e5ae9a8b
keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE 2024-04-17 15:25:20 -04:00
Ryan Heywood 289cec36ef
keyfork wizard: upcast i and index to avoid wrapping add 2024-04-17 15:25:19 -04:00
Ryan Heywood 0fe5301352
keyfork-shard: add in bug messages 2024-04-17 15:25:18 -04:00
Ryan Heywood 9f089e723a
keyfork-derive-openpgp: use .first() in place of .get(0) 2024-04-17 15:25:15 -04:00
Ryan Heywood 1de466cad0
keyfork-derive-util: allow zeroable input for non-master-key derivation 2024-04-17 15:25:02 -04:00
Ryan Heywood 57354fc714
Cargo.lock: bump insta, remove unmaintained yaml-rust 2024-04-14 21:27:57 -04:00
Ryan Heywood 61871a77f0
keyfork-derive-util: make private and public test keys more visible 2024-04-14 21:26:44 -04:00
Ryan Heywood 08a66e2365
keyfork-shard: base64 encode content instead of base16 2024-04-14 21:19:57 -04:00
Ryan Heywood 6fa434e89c
keyfork-shard: shorten length and pad inside encrypted block 2024-04-14 21:19:56 -04:00
Ryan Heywood 68f07f6f02
bump mio and iana-time-zone 2024-04-14 21:19:54 -04:00
Ryan Heywood 9394500f2f
keyfork-shard: generate nonce using hkdf 2024-04-14 21:19:52 -04:00
Ryan Heywood 2bca0a1580
keyfork-derive-util: make Test{Public,Private}Key public, rename Internal algorithm 2024-04-12 16:23:24 -04:00
Ryan Heywood 5438f4e111
keyfork-entropy: downgrade entropy size limit to warning 2024-04-12 16:14:41 -04:00
Ryan Heywood 71b6e4ed0c
Merge branch 'ryan/use-instant-time-qrcode' 2024-04-10 15:35:50 -04:00
Ryan Heywood 4f4e3cfc65
Merge branch 'ryan/harden-derivation-on-highest-level-keys' 2024-04-10 15:35:40 -04:00
Ryan Heywood 194d475d59
keyfork-shard: validate signatures using shard-specific validation requirements 2024-04-10 15:17:30 -04:00
Ryan Heywood 40551a5c26
keyforkd: require hardened derivation on two highest indexes 2024-04-09 20:14:59 -04:00
Ryan Heywood fa125e7cbe
keyfork-qrcode: prefer Instant over SystemTime for infallible time comparison 2024-04-09 19:54:11 -04:00
Ryan Heywood f96ad11422
docs: add basic documentation on shard remote-decrypt protocol 2024-04-08 14:44:26 -04:00
Anton Livaja 089021a302
chore: add link to airgapOS repo 2024-03-28 20:01:50 -04:00
Ryan Heywood 0c768690db
.cargo/config.toml: add registry configuration :) 2024-03-24 22:52:27 -04:00
192 changed files with 6603 additions and 15719 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[registries.distrust]
index = "https://git.distrust.co/public/_cargo-index.git"

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
audits filter=lfs diff=lfs merge=lfs -text

557
CHANGELOG.md Normal file
View File

@ -0,0 +1,557 @@
# Keyfork v0.3.3
This release introduces a checksum verification mechanism for Remote Shard.
### Changes in keyfork-prompt:
```
e7be91b keyfork-{shard,prompt}: add Yes/No prompt for verifying QR codes
```
### Changes in keyfork-shard:
```
e7be91b keyfork-{shard,prompt}: add Yes/No prompt for verifying QR codes
739921d WIP: add checksum to shard
```
# 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
### Changes in keyfork:
```
503c6fa keyfork derive key: initial commit
c46f9e4 move things to use default handler mechanism
92dde3d keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers
```
### Changes in keyfork-crossterm:
```
6317cc9 Cargo.lock: bump deps, dupe generic-array :(
a8b2814 make clippy happy
```
### Changes in keyfork-derive-key:
```
a8b2814 make clippy happy
```
### Changes in keyfork-derive-openpgp:
```
4ab1e8a add docs to make clippy extra happy
a8b2814 make clippy happy
```
### Changes in keyfork-derive-path-data:
```
4ab1e8a add docs to make clippy extra happy
```
### Changes in keyfork-derive-util:
```
a8b2814 make clippy happy
```
### Changes in keyfork-entropy:
```
a8b2814 make clippy happy
```
### Changes in keyfork-mnemonic:
```
a8b2814 make clippy happy
```
### Changes in keyfork-prompt:
```
f8db870 keyfork-prompt: add Headless
92dde3d keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers
a8b2814 make clippy happy
```
### Changes in keyfork-qrcode:
```
be6d562 keyfork-qrcode: use image::ImageReader over image::io::Reader (deprecated)
305e070 Cargo.lock: bump multiple deps to deduplicate
4ab1e8a add docs to make clippy extra happy
a8b2814 make clippy happy
```
### Changes in keyfork-shard:
```
c46f9e4 move things to use default handler mechanism
92dde3d keyfork-prompt: make dyn Trait compatible in prep for allowing dynamic prompt handlers
d7bf3d1 keyfork-shard: move to blahaj
a8b2814 make clippy happy
c36fe0a keyfork-shard: re-enable standard policy, alive check still disabled, add check for encryption keys when discovering certs
```
### Changes in keyfork-zbar:
```
a8b2814 make clippy happy
```
### Changes in keyforkd:
```
c46f9e4 move things to use default handler mechanism
a8b2814 make clippy happy
```
### Changes in keyforkd-client:
```
a8b2814 make clippy happy
```
# Keyfork v0.2.4
This release includes a lot of "maintenance" changes, without any changes in
end-user functionality.
### Changes in keyfork:
The most significant change in this release is the reorganization of some of
the subcommands, where they would be better as enum-traits, such as `keyfork
derive` and `keyfork wizard`.
```
b254ba7 cleanup post-merge
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
5219c5a keyfork: enum-trait-ify choose-your-own commands
b26f296 keyfork-derive-path-data: move all pathcrafting here
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
```
### Changes in keyfork-derive-openpgp:
```
b254ba7 cleanup post-merge
35f57fc Merge branch 'ryansquared/keyfork-mnemonic-refactors'
a2eb5fd bump dependencies with listed vulnerabilities (not affected)
b26f296 keyfork-derive-path-data: move all pathcrafting here
```
### Changes in keyfork-derive-path-data:
This change now centralizes all special Keyfork paths. This means crates should
no longer be required to implement their own path parsing logic.
```
b26f296 keyfork-derive-path-data: move all pathcrafting here
```
### Changes in keyfork-derive-util:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
```
### Changes in keyfork-mnemonic:
`keyfork-mnemonic-util` has finally been renamed to `keyfork-mnemonic`. The
method names `as_bytes() => as_slice()`, `to_bytes() => to_vec()`, and
`into_bytes() => into_vec()`, and the function names
`from_bytes() => try_from_slice()` and
`from_nonstandard_bytes() => from_array()`, have been implemented to more
closely represent the native types they are representing. Additionally,
`Mnemonic::generate_seed()` has been modified to return a constant size array;
this is a breaking change, but should have minimal impact.
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
3ee81b6 keyfork-mnemonic-util: impl as_slice to_vec into_vec
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
```
### Changes in keyfork-prompt:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
```
### Changes in keyfork-shard:
```
58d3c34 Merge branch 'main' into ryansquared/staging-since-latest
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
f5627e5 keyfork-mnemonic-util: impl try_from_slice and from_array
```
### Changes in keyforkd:
```
35ab5e6 keyfork-mnemonic-util => keyfork-mnemonic
02e5b54 keyfork-mnemonic-util::generate_seed: return const size array
536e6da keyforkd{,-client}: lots of documentationings
```
### Changes in keyforkd-client:
```
536e6da keyforkd{,-client}: lots of documentationings
```
# Keyfork v0.2.3
This release includes a bugfix for the wizard where the wizard was too strict
about when keys were "alive".
### Changes in keyfork:
```
dd4354f keyfork: bump keyfork-shard
```
### Changes in keyfork-shard:
```
ba64db8 update Cargo.toml and Cargo.lock
fa84a2a keyfork-shard: Be less strict about keys
```
# Keyfork v0.2.2
This release adds a new wizard, intended to be used at DEFCON 32.
### Changes in keyfork:
```
8d40d26 keyfork: add `bottoms-up` wizard
```
### Changes in keyfork-derive-openpgp:
This change also includes a minor change, allowing the derivation path for
`keyfork-derive-openpg` to derive further than two paths, which was useful in
the testing of the wizard.
```
8d40d26 keyfork: add `bottoms-up` wizard
```
# Keyfork v0.2.1
This release contains an emergency bugfix for Keyfork Shard, which previously
would not be able to properly verify the length of remote shard QR codes.
# Keyfork v0.2.0
Some of the changes in this release are based on feedback from audits
(publications coming soon!). The previous version of Keyfork, in almost every
configuration, is safe to use. The most significant change in this version
affects Keyfork Shard, which has an incompatible difference between this
version and the previous version. Information about shards, such as the length
of the shard, could be leaked and discovered by an attacker when using the
Remote Shard recovery mechanism.
An additional change is the requirement of hardened indices on the first two
levels of key derivation. This is due to Keyfork potentially leaking private
keys when hardened derivation is not used. To be completely honest, I don't
entirely understand the math behind it.
There is no reason to upgrade if Keyfork has been used as-is, as all supported
provisioners at this point in time require hardened derivation at all steps.
### Changes in keyfork:
```
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
5d2309e keyfork-prompt: add SecurePinValidator for making new, secure, PINs
cdf4015 keyfork wizard: use correct derivation path for re-deriving shard decryption keys
f0e5ae9 keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE
289cec3 keyfork wizard: upcast i and index to avoid wrapping add
9394500 keyfork-shard: generate nonce using hkdf
```
### Changes in keyfork-derive-openpgp:
```
f0e5ae9 keyfork-derive-openpgp: document KEYFORK_OPENPGP_EXPIRE
9f089e7 keyfork-derive-openpgp: use .first() in place of .get(0)
```
### Changes in keyfork-derive-util:
```
de4e98a keyfork-derive-util: black-box checking all zeroes
48ccd7c keyfork-derive-util: add note about potential side-channel when verifying keys
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
1de466c keyfork-derive-util: allow zeroable input for non-master-key derivation
61871a7 keyfork-derive-util: make private and public test keys more visible
2bca0a1 keyfork-derive-util: make Test{Public,Private}Key public, rename Internal algorithm
```
### Changes in keyfork-entropy:
```
5438f4e keyfork-entropy: downgrade entropy size limit to warning
```
### Changes in keyfork-mnemonic-util:
```
001fc0b remove trailing hitespace :(
6a265ad keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes
```
### Changes in keyfork-prompt:
```
5d2309e keyfork-prompt: add SecurePinValidator for making new, secure, PINs
```
### Changes in keyfork-qrcode:
```
fa125e7 keyfork-qrcode: prefer Instant over SystemTime for infallible time comparison
```
### Changes in keyfork-shard:
```
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
1a036a0 keyfork-shard: clean up documentation for encrypted shard padding
e068743 keyfork-shard: display error message on duplicate key fingerprints found
23db509 keyfork-shard: improve wording for counting shardholders
9461772 keyfork-shard: ignore duplicate certificate entries
6a265ad keyfork-mnemonic-util: add MnemonicBase::from_nonstandard_bytes
c0b19e2 keyfork-shard: assert shared secrets are contributory
0fe5301 keyfork-shard: add in bug messages
08a66e2 keyfork-shard: base64 encode content instead of base16
6fa434e keyfork-shard: shorten length and pad inside encrypted block
9394500 keyfork-shard: generate nonce using hkdf
194d475 keyfork-shard: validate signatures using shard-specific validation requirements
```
### Changes in keyfork-zbar:
```
0c76869 .cargo/config.toml: add registry configuration :)
```
### Changes in keyforkd:
```
bcfcc87 keyforkd: add warning when loading seed with less than 128 bits
40551a5 keyforkd: require hardened derivation on two highest indexes
```
### Changes in keyforkd-client:
```
d04989e keyfork-derive-util: make key parsing fallible again, since secp256k1 isn't guaranteed correct
1de466c keyfork-derive-util: allow zeroable input for non-master-key derivation
40551a5 keyforkd: require hardened derivation on two highest indexes
```
### Changes in keyforkd-models:
```
40551a5 keyforkd: require hardened derivation on two highest indexes
```
# Keyfork v0.1.0
### Tagged releases:
* `keyfork-bin 0.1.0`
* `keyfork-bug 0.1.0`
* `keyfork-crossterm 0.27.1`
* `keyfork-derive-key 0.1.0`
* `keyfork-derive-openpgp 0.1.0`
* `keyfork-derive-path-data 0.1.0`
* `keyfork-derive-util 0.1.0`
* `keyfork-entropy 0.1.0`
* `keyfork-frame 0.1.0`
* `keyfork-mnemonic-util 0.2.0`
* `keyfork-prompt 0.1.0`
* `keyfork-qrcode 0.1.0`
* `keyfork-shard 0.1.0`
* `keyfork-slip10-test-data 0.1.0`
* `keyfork 0.1.0`
* `keyfork-zbar-sys 0.1.0`
* `keyfork-zbar 0.1.0`
* `keyforkd-client 0.1.0`
* `keyforkd-models 0.1.0`
* `keyforkd 0.1.0`
* `smex 0.1.0`

2386
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,16 +16,93 @@ members = [
"crates/qrcode/keyfork-zbar-sys",
"crates/util/keyfork-bin",
"crates/util/keyfork-bug",
"crates/util/keyfork-crossterm",
"crates/util/keyfork-crossterm-ioctl-shim",
"crates/util/keyfork-entropy",
"crates/util/keyfork-frame",
"crates/util/keyfork-mnemonic-util",
"crates/util/keyfork-mnemonic",
"crates/util/keyfork-prompt",
"crates/util/keyfork-slip10-test-data",
"crates/util/smex",
"crates/tests",
]
[workspace.lints.rust]
missing_docs = { level = "warn" }
[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }
missing_errors_doc = { level = "warn" }
missing_panics_doc = { level = "warn" }
# used often in tests
wildcard_imports = { level = "allow"}
# annoying
must_use_candidate = "allow"
return_self_not_must_use = "allow"
# sometimes i like the logical flow of keeping things in an "else"
redundant_else = "allow"
# i hate using `.unwrap_or_else(|| keyfork_bug::bug!())`
expect_fun_call = "allow"
[workspace.dependencies]
# Keyfork dependencies
keyforkd = { version = "0.1.1", path = "crates/daemon/keyforkd", registry = "distrust", default-features = false }
keyforkd-client = { version = "0.2.0", path = "crates/daemon/keyforkd-client", registry = "distrust", default-features = false }
keyforkd-models = { version = "0.2.0", path = "crates/daemon/keyforkd-models", registry = "distrust", default-features = false }
keyfork-derive-openpgp = { version = "0.1.2", path = "crates/derive/keyfork-derive-openpgp", registry = "distrust", default-features = false }
keyfork-derive-path-data = { version = "0.1.1", path = "crates/derive/keyfork-derive-path-data", registry = "distrust", default-features = false }
keyfork-derive-util = { version = "0.2.0", path = "crates/derive/keyfork-derive-util", registry = "distrust", default-features = false }
keyfork-shard = { version = "0.3.4", path = "crates/keyfork-shard", registry = "distrust", default-features = false }
keyfork-qrcode = { version = "0.1.1", path = "crates/qrcode/keyfork-qrcode", registry = "distrust", default-features = false }
keyfork-zbar = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar", registry = "distrust", default-features = false }
keyfork-zbar-sys = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar-sys", registry = "distrust", default-features = false }
keyfork-bin = { version = "0.1.0", path = "crates/util/keyfork-bin", registry = "distrust", default-features = false }
keyfork-bug = { version = "0.1.1", path = "crates/util/keyfork-bug", registry = "distrust", default-features = false }
keyfork-entropy = { version = "0.1.1", path = "crates/util/keyfork-entropy", registry = "distrust", default-features = false }
keyfork-frame = { version = "0.1.0", path = "crates/util/keyfork-frame", registry = "distrust", default-features = false }
keyfork-mnemonic = { version = "0.4.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false }
keyfork-prompt = { version = "0.2.3", path = "crates/util/keyfork-prompt", registry = "distrust", default-features = false }
keyfork-slip10-test-data = { version = "0.1.0", path = "crates/util/keyfork-slip10-test-data", registry = "distrust", default-features = false }
smex = { version = "0.1.0", path = "crates/util/smex", registry = "distrust", default-features = false }
# External dependencies
# Cryptography
ed25519-dalek = "2.1.1"
hmac = "0.12.1"
k256 = { version = "0.13.3", default-features = false, features = ["std"] }
sha2 = "0.10.8"
# OpenPGP
card-backend-pcsc = "0.5.0"
openpgp-card = { version = "0.4.1" }
openpgp-card-sequoia = { version = "0.2.0", default-features = false }
sequoia-openpgp = { version = "1.21.2", default-features = false, features = ["compression"] }
# Serialization
bincode = "1.3.3"
serde = { version= "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
# Misc.
anyhow = "1.0.79"
hex-literal = "1.0.0"
image = { version = "0.25.2", default-features = false }
thiserror = "1.0.56"
tokio = "1.35.1"
v4l = "0.14.0"
base64 = "0.22.1"
tempfile = "3.17.1"
[profile.release]
debug = true
[profile.dev.package.keyfork-qrcode]
opt-level = 3
debug = true

11
MAINTENANCE.md Normal file
View File

@ -0,0 +1,11 @@
# Releasing new versions
* 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
the file.
* Make sure to add some human-readable snippets at the top!
* Update all versions of crates listed in the changelog.
* Commit changes.
* Run the `sign-new-versions.sh` script to tag the new versions.
* Run the `publish.sh` script to push the latest packages to the Distrust
Cargo registry.

View File

@ -34,7 +34,7 @@ review:
$(eval HEAD_REF_PARSED := $(shell git rev-parse $(HEAD_REF)))
@echo "Ensuring current HEAD_REF is not BASE_REF"
test "$(BASE_REF_PARSED)" != "$(HEAD_REF_PARSED)"
@echo "Verifying if any changes happened in Cargo.lock that require review; otherwise, use `git difftool` directly"
@echo "Verifying if any changes happened in Cargo.lock that require review; otherwise, use: git difftool"
test "$(shell git show $(BASE_REF_PARSED):Cargo.lock | sha256sum)" != "$(shell git show $(HEAD_REF_PARSED):Cargo.lock | sha256sum)"
$(eval TEMP_REPO := $(shell mktemp -d))
$(call clone-repo,$(TEMP_REPO),$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))),$(BASE_REF_PARSED))

View File

@ -75,7 +75,7 @@ Note: The following features are proposed, and may not yet be implemented.
* Offline
* Will exit if network access is detected to force you to keep keys offline
* Helps limit the risk of supply chain attacks
* Intended for use with QubesOS Vault VM, AirgapOS, etc
* Intended for use with QubesOS Vault VM, [AirgapOS](https://git.distrust.co/public/airgap), etc
* Private keys are installed to HSMs/TEEs for use by online machines
## Install
@ -178,7 +178,8 @@ keyfork recover mnemonic
This guide assumes you are sharding to an `N`-of-`M` system with `I` smart
cards per shardholder. The variables will be used in the following commands as
`$N`, `$M`, and `$I`. The smart cards will be factory reset during the process.
`$N`, `$M`, and `$I`. The smart card OpenPGP slots will be factory reset during
the process.
On an airgapped system, run the following command to generate a file containing
encrypted shards of a generated seed:

BIN
audits/DIS-01-report.v2.pdf Normal file

Binary file not shown.

92
bacon.toml Normal file
View File

@ -0,0 +1,92 @@
# This is a configuration file for the bacon tool
#
# Bacon repository: https://github.com/Canop/bacon
# Complete help on configuration: https://dystroy.org/bacon/config/
# You can also check bacon's own bacon.toml file
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
default_job = "check"
[jobs.check]
command = ["cargo", "check", "--color", "always"]
need_stdout = false
[jobs.check-all]
command = ["cargo", "check", "--all-targets", "--color", "always"]
need_stdout = false
[jobs.clippy]
command = [
"cargo", "clippy",
"--all-targets",
"--color", "always",
]
need_stdout = false
[jobs.clippy-unwrap]
command = [
"cargo", "clippy",
"--lib",
"--color", "always",
"--",
"-W",
"clippy::unwrap_used",
"-W",
"clippy::expect_used",
]
need_stdout = false
# This job lets you run
# - all tests: bacon test
# - a specific test: bacon test -- config::test_default_files
# - the tests of a package: bacon test -- -- -p config
[jobs.test]
command = [
"cargo", "test", "--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"]
need_stdout = false
# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change
# You can run your application and have the result displayed in bacon,
# *if* it makes sense for this crate.
# Don't forget the `--color always` part or the errors won't be
# properly parsed.
# If your program never stops (eg a server), you may set `background`
# to false to have the cargo run output immediately displayed instead
# of waiting for program's end.
[jobs.run]
command = [
"cargo", "run",
"--color", "always",
# put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
background = true
# This parameterized job runs the example of your choice, as soon
# as the code compiles.
# Call it as
# bacon ex -- my-example
[jobs.ex]
command = ["cargo", "run", "--color", "always", "--example"]
need_stdout = true
allow_warnings = true
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"

1
clippy.toml Normal file
View File

@ -0,0 +1 @@
doc-valid-idents = ["OpenPGP", ".."]

View File

@ -1,9 +1,12 @@
[package]
name = "keyforkd-client"
version = "0.1.0"
version = "0.2.2"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
@ -12,14 +15,14 @@ ed25519 = ["keyfork-derive-util/ed25519", "ed25519-dalek"]
secp256k1 = ["keyfork-derive-util/secp256k1", "k256"]
[dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false, registry = "distrust" }
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame", registry = "distrust" }
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models", registry = "distrust" }
bincode = "1.3.3"
thiserror = "1.0.49"
k256 = { version = "0.13.3", optional = true }
ed25519-dalek = { version = "2.1.1", optional = true }
keyfork-derive-util = { workspace = true, default-features = false }
keyfork-frame = { workspace = true }
keyforkd-models = { workspace = true }
bincode = { workspace = true }
thiserror = { workspace = true }
k256 = { workspace = true, default-features = false, features = ["std"], optional = true }
ed25519-dalek = { workspace = true, optional = true }
[dev-dependencies]
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data", registry = "distrust" }
keyforkd = { path = "../keyforkd", registry = "distrust" }
keyfork-slip10-test-data = { workspace = true }
keyforkd = { workspace = true }

View File

@ -1,8 +1,10 @@
//! # The Keyforkd Client
//!
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". The server is operated
//! on a UNIX socket with messages sent using the Keyfork Frame format.
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". This allows
//! enforcement of policies, such as requiring at least two leves of a derivation path (for
//! instance, `m/0'` would not be allowed, but `m/0'/0'` would). The server is operated on a UNIX
//! socket with messages sent using the Keyfork Frame format.
//!
//! Programs using the Keyfork Client should ensure they are built against a compatible version of
//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
@ -10,18 +12,23 @@
//! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but
//! `1.x.y` will not be compatible with `2.0.0`.
//!
//! Presently, the Keyfork server only supports the following requests:
//! The Keyfork Client documentation makes extensive use of the `keyforkd::test_util` module.
//! This provides testing infrastructure to set up a temporary Keyfork Daemon. In
//! your code, you should assume the daemon has already been initialized, whether by another
//! process, on another terminal, or some other instance. At no point should a program deriving an
//! "endpoint" key have control over a mnemonic or a seed.
//!
//! * Derive Key
//! ## Server Requests
//!
//! ## Extended Private Keys
//!
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed.
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
//! will be derived similarly between the server and the client.
//! Keyfork is designed as a client-request/server-response model. The client sends a request, such
//! as a derivation request, and the server sends its response. Presently, the Keyfork server
//! supports the following requests:
//!
//! ### Request: Derive Key
//!
//! The client creates a derivation path of at least two indices and requests a derived `XPrv`
//! (Extended Private Key) from the server.
//!
//! # Examples
//! ```rust
//! use std::str::FromStr;
//!
@ -31,17 +38,121 @@
//! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey as PrivateKey;
//!
//! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| {
//! # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
//! let mut client = Client::discover_socket().unwrap();
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
//! # keyforkd::test_util::Infallible::Ok(())
//! # }).unwrap();
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! # Ok::<_, Error>(())
//! # })?;
//! Ok(())
//! }
//! ```
//!
//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used.
//! ---
//!
//! Request objects are typically handled by the Keyfork Client library (such as with
//! [`Client::request_xprv`]). While unadvised, clients can also attempt to handle their own
//! requests, using [`Client::request`].
//!
//! ## Extended Private Keys
//!
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
//! Private Key (often shortened to `XPrv`) has been created, further derivations can be performed.
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
//! will be derived similarly between the server and the client.
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::{DerivationIndex, DerivationPath};
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey as PrivateKey;
//! # fn check_wallet<T>(_: T) {}
//!
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Index(#[from] keyfork_derive_util::IndexError),
//!
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! PrivateKey(#[from] keyfork_derive_util::PrivateKeyError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'/0'/0")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! // scan first 20 wallets
//! for index in 0..20 {
//! // use non-hardened derivation
//! let new_xprv = xprv.derive_child(&DerivationIndex::new(index, false)?);
//! check_wallet(new_xprv)
//! }
//! # Ok::<_, Error>(())
//! # })?;
//! Ok(())
//! }
//! ```
//!
//! ## Testing Infrastructure
//!
//! In tests, the `keyforkd::test_util` module and `TestPrivateKeys` can be used. These provide
//! useful utilities for writing tests that interact with the Keyfork Server without needing to
//! manually create the server for the purpose of the test. The `run_test` method can be used to
//! run a test, which can handle both returning errors and correctly translating panics (though,
//! the panics definitely won't look tidy).
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//!
//! #[derive(Debug, thiserror::Error)]
//! enum Error {
//! #[error(transparent)]
//! Path(#[from] keyfork_derive_util::PathError),
//!
//! #[error(transparent)]
//! Keyforkd(#[from] keyforkd_client::Error),
//! }
//!
//! fn main() -> Result<(), Error> {
//! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| {
//! let derivation_path = DerivationPath::from_str("m/44'/0'")?;
//! let mut client = Client::discover_socket()?;
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path)?;
//! Ok::<_, Error>(())
//! })?;
//! Ok(())
//! }
//! ```
//!
//! If you would rather write tests to panic rather than error, or would rather not deal with error
//! types, the Panicable type should be used, which will handle the Error type for the closure.
//!
//! ```rust
//! use std::str::FromStr;
@ -52,11 +163,10 @@
//!
//! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| {
//! std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
//! let mut client = Client::discover_socket().unwrap();
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
//! keyforkd::test_util::Infallible::Ok(())
//! keyforkd::test_util::Panicable::Ok(())
//! }).unwrap();
//! ```
@ -89,6 +199,10 @@ pub enum Error {
#[error("Socket was unable to connect to {1}: {0} (make sure keyforkd is running)")]
Connect(std::io::Error, PathBuf),
/// The path of the derived key was of an invalid length.
#[error("Derived key path is of invalid length")]
InvalidPathLength(#[from] std::num::TryFromIntError),
/// Data could not be written to, or read from, the socket.
#[error("Could not write to or from the socket: {0}")]
Io(#[from] std::io::Error),
@ -108,6 +222,10 @@ pub enum Error {
/// An error encountered in Keyforkd.
#[error("Error in Keyforkd: {0}")]
Keyforkd(#[from] KeyforkdError),
/// An invalid key was returned.
#[error("Invalid key returned")]
InvalidKey,
}
#[allow(missing_docs)]
@ -123,19 +241,15 @@ pub fn get_socket() -> Result<UnixStream, Error> {
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
.collect::<HashMap<String, String>>();
let mut socket_path: PathBuf;
#[allow(clippy::single_match_else)]
match socket_vars.get("KEYFORKD_SOCKET_PATH") {
Some(occupied) => {
socket_path = PathBuf::from(occupied);
}
None => {
socket_path = PathBuf::from(
socket_vars
.get("XDG_RUNTIME_DIR")
.ok_or(Error::EnvVarsNotFound)?,
);
socket_path.extend(["keyforkd", "keyforkd.sock"]);
}
if let Some(occupied) = socket_vars.get("KEYFORKD_SOCKET_PATH") {
socket_path = PathBuf::from(occupied);
} else {
socket_path = PathBuf::from(
socket_vars
.get("XDG_RUNTIME_DIR")
.ok_or(Error::EnvVarsNotFound)?,
);
socket_path.extend(["keyforkd", "keyforkd.sock"]);
}
UnixStream::connect(&socket_path).map_err(|e| Error::Connect(e, socket_path))
}
@ -152,7 +266,7 @@ pub struct Client {
impl Client {
/// Create a new client from a given already-connected [`UnixStream`]. This function is
/// provided in case a specific UnixStream has to be used; otherwise,
/// provided in case a specific `UnixStream` has to be used; otherwise,
/// [`Client::discover_socket`] should be preferred.
///
/// # Examples
@ -161,10 +275,9 @@ impl Client {
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let mut socket = get_socket().unwrap();
/// let mut socket = get_socket()?;
/// let mut client = Client::new(socket);
/// # keyforkd::test_util::Infallible::Ok(())
/// # Ok::<_, keyforkd_client::Error>(())
/// # }).unwrap();
/// ```
pub fn new(socket: UnixStream) -> Self {
@ -183,9 +296,8 @@ impl Client {
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let mut client = Client::discover_socket().unwrap();
/// # keyforkd::test_util::Infallible::Ok(())
/// let mut client = Client::discover_socket()?;
/// # Ok::<_, keyforkd_client::Error>(())
/// # }).unwrap();
/// ```
pub fn discover_socket() -> Result<Self> {
@ -213,11 +325,10 @@ impl Client {
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
/// let mut client = Client::discover_socket().unwrap();
/// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
/// # keyforkd::test_util::Infallible::Ok(())
/// # keyforkd::test_util::Panicable::Ok(())
/// # }).unwrap();
/// ```
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
@ -233,25 +344,27 @@ impl Client {
return Err(Error::InvalidResponse);
}
let depth = path.len() as u8;
Ok(ExtendedPrivateKey::new_from_parts(
&d.data,
depth,
d.chain_code,
))
let depth = u8::try_from(path.len())?;
ExtendedPrivateKey::from_parts(&d.data, depth, d.chain_code)
.map_err(|_| Error::InvalidKey)
}
_ => Err(Error::InvalidResponse),
}
}
}
impl Client {
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
///
/// This function does not properly assert the association between a request type and a
/// response type, and does not perform any serialization of native objects into Request or
/// Response types, and should only be used when absolutely necessary.
///
/// # Errors
/// An error may be returned if:
/// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd.
#[doc(hidden)]
pub fn request(&mut self, req: &Request) -> Result<Response> {
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
let resp = try_decode_from(&mut self.socket)?;

View File

@ -1,7 +1,7 @@
use crate::Client;
use keyfork_derive_util::{request::*, DerivationPath};
use keyfork_slip10_test_data::test_data;
use keyforkd::test_util::{run_test, Infallible};
use keyforkd::test_util::{run_test, Panicable};
use std::{os::unix::net::UnixStream, str::FromStr};
#[test]
@ -9,55 +9,58 @@ use std::{os::unix::net::UnixStream, str::FromStr};
fn secp256k1_test_suite() {
use k256::SecretKey;
let tests = test_data()
.unwrap()
.remove(&"secp256k1".to_string())
.unwrap();
let tests = test_data().unwrap().remove("secp256k1").unwrap();
for seed_test in tests {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
for test in seed_test.tests {
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len();
if chain_len < 2 {
continue;
}
// Consistency check: ensure the server and the client can each derive the same
// key using an XPrv, for all but the last XPrv, which is verified after this
for i in 2..chain_len {
// FIXME: Keyfork will only allow one request per session
run_test(
&seed,
move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
for test in seed_test.tests {
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let path = DerivationPath::from_str(test.chain).unwrap();
let left_path = path.inner()[..i]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let right_path = path.inner()[i..]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
let derived_xprv = xprv.derive_path(&right_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
assert_eq!(
derived_xprv, keyforkd_xprv,
"{left_path} + {right_path} != {path}"
let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len();
if chain_len < 2 {
continue;
}
if chain.iter().take(2).any(|index| !index.is_hardened()) {
continue;
}
// Consistency check: ensure the server and the client can each derive the same
// key using an XPrv, for all but the last XPrv, which is verified after this
for i in 2..chain_len {
// FIXME: Keyfork will only allow one request per session
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let path = DerivationPath::from_str(test.chain).unwrap();
let left_path = path.inner()[..i]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let right_path = path.inner()[i..]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
let derived_xprv = xprv.derive_path(&right_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
assert_eq!(
derived_xprv, keyforkd_xprv,
"{left_path} + {right_path} != {path}"
);
}
let req = DerivationRequest::new(
DerivationAlgorithm::Secp256k1,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(&response.data, test.private_key.as_slice());
}
let req = DerivationRequest::new(
DerivationAlgorithm::Secp256k1,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(&response.data, test.private_key.as_slice());
}
Ok(())
})
Ok(())
},
)
.unwrap();
}
}
@ -67,7 +70,7 @@ fn secp256k1_test_suite() {
fn ed25519_test_suite() {
use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
let tests = test_data().unwrap().remove("ed25519").unwrap();
for seed_test in tests {
let seed = seed_test.seed;
@ -106,7 +109,7 @@ fn ed25519_test_suite() {
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(&response.data, test.private_key.as_slice());
}
Infallible::Ok(())
Panicable::Ok(())
})
.unwrap();
}

View File

@ -1,12 +1,15 @@
[package]
name = "keyforkd-models"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false, registry = "distrust" }
serde = { version = "1.0.190", features = ["derive"] }
thiserror = "1.0.50"
keyfork-derive-util = { workspace = true, default-features = false }
serde = { workspace = true }
thiserror = { workspace = true }

View File

@ -43,6 +43,10 @@ pub enum DerivationError {
#[error("Invalid derivation length: Expected at least 2, actual: {0}")]
InvalidDerivationLength(usize),
/// The derivation request did not use hardened derivation on the 2 highest indexes.
#[error("Invalid derivation paths: expected index #{0} (1) to be hardened")]
InvalidDerivationPath(usize, u32),
/// An error occurred while deriving data.
#[error("Derivation error: {0}")]
Derivation(String),

View File

@ -1,9 +1,12 @@
[package]
name = "keyforkd"
version = "0.1.0"
version = "0.1.4"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
@ -12,28 +15,28 @@ tracing = ["tower/tracing", "tokio/tracing", "dep:tracing", "dep:tracing-subscri
multithread = ["tokio/rt-multi-thread"]
[dependencies]
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", registry = "distrust" }
keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame", features = ["async"], registry = "distrust" }
keyfork-mnemonic-util = { version = "0.2.0", path = "../../util/keyfork-mnemonic-util", registry = "distrust" }
keyfork-derive-path-data = { version = "0.1.0", path = "../../derive/keyfork-derive-path-data", registry = "distrust" }
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models", registry = "distrust" }
keyfork-bug = { workspace = true }
keyfork-derive-util = { workspace = true }
keyfork-frame = { workspace = true, features = ["async"] }
keyfork-mnemonic = { workspace = true }
keyfork-derive-path-data = { workspace = true }
keyforkd-models = { workspace = true }
# Not personally audited
bincode = "1.3.3"
bincode = { workspace = true }
# Ecosystem trust, not personally audited
tokio = { version = "1.32.0", features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] }
tokio = { workspace = true, features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] }
tracing = { version = "0.1.37", optional = true }
tracing-error = { version = "0.2.0", optional = true }
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] }
tower = { version = "0.4.13", features = ["tokio", "util"] }
tower = { version = "0.5.0", features = ["tokio", "util"], default-features = false }
# Personally audited
thiserror = "1.0.47"
serde = { version = "1.0.186", features = ["derive"] }
tempfile = { version = "3.10.0", default-features = false }
thiserror = { workspace = true }
serde = { workspace = true }
tempfile = { workspace = true }
[dev-dependencies]
hex-literal = "0.4.1"
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data", registry = "distrust" }
hex-literal = { workspace = true }
keyfork-slip10-test-data = { workspace = true }

View File

@ -5,7 +5,7 @@ use std::{
path::{Path, PathBuf},
};
pub use keyfork_mnemonic_util::Mnemonic;
pub use keyfork_mnemonic::Mnemonic;
pub use tower::ServiceBuilder;
#[cfg(feature = "tracing")]
@ -57,7 +57,7 @@ pub async fn start_and_run_server_on(
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
// TODO: passphrase support and/or store passphrase with mnemonic
.service(Keyforkd::new(mnemonic.generate_seed(None)));
.service(Keyforkd::new(mnemonic.generate_seed(None).to_vec()));
let mut server = match UnixServer::bind(socket_path) {
Ok(s) => s,
@ -89,27 +89,23 @@ pub async fn start_and_run_server(mnemonic: Mnemonic) -> Result<(), Box<dyn std:
let runtime_vars = std::env::vars()
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
.collect::<HashMap<String, String>>();
let mut runtime_path: PathBuf;
#[allow(clippy::single_match_else)]
match runtime_vars.get("KEYFORKD_SOCKET_PATH") {
Some(occupied) => {
runtime_path = PathBuf::from(occupied);
let runtime_path = if let Some(occupied) = runtime_vars.get("KEYFORKD_SOCKET_PATH") {
PathBuf::from(occupied)
} else {
let mut runtime_path = PathBuf::from(
runtime_vars
.get("XDG_RUNTIME_DIR")
.ok_or(KeyforkdError::NoSocketPath)?,
);
runtime_path.push("keyforkd");
#[cfg(feature = "tracing")]
debug!("ensuring directory exists: {}", runtime_path.display());
if !runtime_path.is_dir() {
tokio::fs::create_dir(&runtime_path).await?;
}
None => {
runtime_path = PathBuf::from(
runtime_vars
.get("XDG_RUNTIME_DIR")
.ok_or(KeyforkdError::NoSocketPath)?,
);
runtime_path.push("keyforkd");
#[cfg(feature = "tracing")]
debug!("ensuring directory exists: {}", runtime_path.display());
if !runtime_path.is_dir() {
tokio::fs::create_dir(&runtime_path).await?;
}
runtime_path.push("keyforkd.sock");
}
}
runtime_path.push("keyforkd.sock");
runtime_path
};
#[cfg(feature = "tracing")]
debug!(

View File

@ -1,6 +1,6 @@
//!
//! Launch the Keyfork Server from using a mnemonic passed through standard input.
use keyfork_mnemonic_util::Mnemonic;
use keyfork_mnemonic::Mnemonic;
use tokio::io::{self, AsyncBufReadExt, BufReader};

View File

@ -11,7 +11,7 @@ pub struct BincodeLayer<'a, Request> {
phantom_request: PhantomData<&'a Request>,
}
impl<'a, Request> BincodeLayer<'a, Request> {
impl<Request> BincodeLayer<'_, Request> {
/// Create a new [`BincodeLayer`].
pub fn new() -> Self {
Self {
@ -21,7 +21,7 @@ impl<'a, Request> BincodeLayer<'a, Request> {
}
}
impl<'a, Request> Default for BincodeLayer<'a, Request> {
impl<Request> Default for BincodeLayer<'_, Request> {
fn default() -> Self {
Self::new()
}
@ -162,6 +162,9 @@ mod tests {
.call(content.clone())
.await
.unwrap();
assert_eq!(result, serialize(&Result::<Test, Infallible>::Ok(test)).unwrap());
assert_eq!(
result,
serialize(&Result::<Test, Infallible>::Ok(test)).unwrap()
);
}
}

View File

@ -49,7 +49,8 @@ impl IsDisconnect for EncodeError {
}
impl UnixServer {
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also creates a ctrl_c handler to automatically clean up the socket file.
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also
/// creates a `ctrl_c` handler to automatically clean up the socket file.
///
/// # Errors
/// This function may return an error if the socket can't be bound.

View File

@ -12,7 +12,7 @@ use keyfork_derive_path_data::guess_target;
// use keyfork_derive_util::request::{DerivationError, DerivationRequest, DerivationResponse};
use keyforkd_models::{DerivationError, Error, Request, Response};
use tower::Service;
use tracing::info;
use tracing::{info, warn};
// NOTE: All values implemented in Keyforkd must implement Clone with low overhead, either by
// using an Arc or by having a small signature. This is because Service<T> takes &mut self.
@ -38,6 +38,12 @@ impl std::fmt::Debug for Keyforkd {
impl Keyforkd {
/// Create a new instance of Keyfork from a given seed.
pub fn new(seed: Vec<u8>) -> Self {
if seed.len() < 16 {
warn!(
"Entropy size is lower than 128 bits: {} bits.",
seed.len() * 8
);
}
Self {
seed: Arc::new(seed),
}
@ -69,6 +75,18 @@ impl Service<Request> for Keyforkd {
return Err(DerivationError::InvalidDerivationLength(len).into());
}
if let Some((i, unhardened_index)) = req
.path()
.iter()
.take(2)
.enumerate()
.find(|(_, index)| !index.is_hardened())
{
return Err(
DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into(),
);
}
#[cfg(feature = "tracing")]
if let Some(target) = guess_target(req.path()) {
info!("Deriving path: {target}");
@ -96,10 +114,7 @@ mod tests {
#[tokio::test]
async fn properly_derives_secp256k1() {
let tests = test_data()
.unwrap()
.remove(&"secp256k1".to_string())
.unwrap();
let tests = test_data().unwrap().remove("secp256k1").unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -110,6 +125,9 @@ mod tests {
if chain.len() < 2 {
continue;
}
if chain.iter().take(2).any(|index| !index.is_hardened()) {
continue;
}
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response: DerivationResponse = keyforkd
.ready()
@ -128,7 +146,7 @@ mod tests {
#[tokio::test]
async fn properly_derives_ed25519() {
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
let tests = test_data().unwrap().remove("ed25519").unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -156,7 +174,7 @@ mod tests {
}
}
#[should_panic]
#[should_panic(expected = "InvalidDerivationLength(0)")]
#[tokio::test]
async fn errors_on_no_path() {
let tests = [(
@ -182,7 +200,7 @@ mod tests {
}
}
#[should_panic]
#[should_panic(expected = "InvalidDerivationLength(1)")]
#[tokio::test]
async fn errors_on_short_path() {
let tests = [(

View File

@ -12,20 +12,21 @@ use keyfork_bug::bug;
#[derive(Debug, thiserror::Error)]
#[error("This error can never be instantiated")]
#[doc(hidden)]
pub struct InfallibleError {
protected: (),
}
pub enum UninstantiableError {}
/// An infallible result. This type can be used to represent a function that should never error.
/// A panicable result. This type can be used when a closure chooses to panic instead of
/// returning an error. This doesn't necessarily mean a closure _has_ to panic, and its absence
/// doesn't imply a closure _can't_ panic, but this is a useful utility function for writing tests,
/// to avoid the necessity of making custom error types.
///
/// ```rust
/// use keyforkd::test_util::Infallible;
/// use keyforkd::test_util::Panicable;
/// let closure = || {
/// Infallible::Ok(())
/// Panicable::Ok(())
/// };
/// assert!(closure().is_ok());
/// ```
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
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
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
@ -39,6 +40,8 @@ pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// runtime.
///
/// # Examples
/// The test utility provides a socket that can be connected to for deriving keys.
///
/// ```rust
/// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
@ -46,10 +49,22 @@ pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// UnixStream::connect(&path).map(|_| ())
/// }).unwrap();
/// ```
///
/// The `keyforkd-client` crate uses the `KEYFORKD_SOCKET_PATH` variable to determine the default
/// socket path. The test will export the environment variable so it may be used by default.
///
/// ```rust
/// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
/// assert_eq!(std::env::var_os("KEYFORKD_SOCKET_PATH").unwrap(), path.as_os_str());
/// UnixStream::connect(&path).map(|_| ())
/// }).unwrap();
/// ```
#[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
pub fn run_test<F, E>(seed: &[u8], closure: F) -> std::result::Result<(), E>
where
F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
F: FnOnce(&std::path::Path) -> std::result::Result<(), E> + Send + 'static,
E: Send + 'static,
{
let rt = Builder::new_multi_thread()
@ -61,7 +76,7 @@ where
));
let socket_dir = tempfile::tempdir().expect(bug!("can't create tempdir"));
let socket_path = socket_dir.path().join("keyforkd.sock");
rt.block_on(async move {
let result = rt.block_on(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let server_handle = tokio::spawn({
let socket_path = socket_path.clone();
@ -74,21 +89,30 @@ where
.expect(bug!("couldn't send server start signal"));
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
.service(Keyforkd::new(seed.to_vec()));
server.run(service).await.expect(bug!("Unable to start service"));
.service(Keyforkd::new(seed.clone()));
server
.run(service)
.await
.expect(bug!("Unable to start service"));
}
});
rx.recv()
.await
.expect(bug!("can't receive server start signal from channel"));
std::env::set_var("KEYFORKD_SOCKET_PATH", &socket_path);
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
let result = test_handle.await;
server_handle.abort();
result
})
.expect(bug!("runtime could not join all threads"))
});
if let Err(e) = result {
if let Ok(reason) = e.try_into_panic() {
std::panic::resume_unwind(reason);
}
}
Ok(())
}
#[cfg(test)]
@ -98,6 +122,6 @@ mod tests {
#[test]
fn test_run_test() {
let seed = b"beefbeef";
run_test(seed, |_path| Infallible::Ok(())).expect("infallible");
run_test(seed, |_path| Panicable::Ok(())).expect("infallible");
}
}

View File

@ -0,0 +1,19 @@
[package]
name = "keyfork-derive-age"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] }
keyforkd-client = { workspace = true }
smex = { workspace = true }
thiserror = "1.0.48"
bech32 = "0.11.0"
keyfork-derive-path-data = { workspace = true }
ed25519-dalek = "2.1.1"

View File

@ -0,0 +1,69 @@
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_path_data::paths;
use keyfork_derive_util::{DerivationPath, ExtendedPrivateKey, PathError};
use keyforkd_client::Client;
use ed25519_dalek::SigningKey;
type XPrv = ExtendedPrivateKey<SigningKey>;
/// Any error that can occur while deriving a key.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The given path could not be parsed.
#[error("Could not parse the given path: {0}")]
PathFormat(#[from] PathError),
/// The request to derive data failed.
#[error("Unable to perform key derivation request: {0}")]
KeyforkdClient(#[from] keyforkd_client::Error),
}
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
fn validate(path: &str) -> Result<DerivationPath> {
let index = paths::AGE.inner().first().unwrap();
let path = DerivationPath::from_str(path)?;
assert!(
path.len() >= 2,
"Expected path of at least m/{index}/account_id'"
);
let given_index = path.iter().next().expect("checked .len() above");
assert_eq!(
index, given_index,
"Expected derivation path starting with m/{index}, got: {given_index}",
);
Ok(path)
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let path = match args.as_slice() {
[path] => validate(path)?,
_ => panic!("Usage: {program_name} path"),
};
let mut client = Client::discover_socket()?;
// TODO: should this key be clamped to Curve25519 specs?
let xprv: XPrv = client.request_xprv(&path)?;
let hrp = bech32::Hrp::parse("AGE-SECRET-KEY-")?;
let age_key = bech32::encode::<bech32::Bech32>(hrp, &xprv.private_key().to_bytes())?;
println!("{}", age_key.to_uppercase());
Ok(())
}
fn main() -> ExitCode {
if let Err(e) = run() {
eprintln!("Error: {e}");
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}

View File

@ -1,13 +1,16 @@
[package]
name = "keyfork-derive-key"
version = "0.1.0"
version = "0.1.2"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", registry = "distrust" }
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", registry = "distrust" }
smex = { version = "0.1.0", path = "../../util/smex", registry = "distrust" }
thiserror = "1.0.48"
keyfork-derive-util = { workspace = true }
keyforkd-client = { workspace = true }
smex = { workspace = true }
thiserror = { workspace = true }

View File

@ -1,4 +1,4 @@
//!
//! Query the Keyfork Server to generate a hex-encoded key for a given algorithm.
use std::{env, process::ExitCode, str::FromStr};
@ -46,7 +46,10 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::discover_socket()?;
let request = DerivationRequest::new(algo, &path);
let response = client.request(&request.into())?;
println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
println!(
"{}",
smex::encode(DerivationResponse::try_from(response)?.data)
);
Ok(())
}

View File

@ -1,18 +1,22 @@
[package]
name = "keyfork-derive-openpgp"
version = "0.1.0"
version = "0.1.5"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["bin"]
bin = ["sequoia-openpgp/crypto-nettle"]
[dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"], registry = "distrust" }
keyforkd-client = { version = "0.1.0", path = "../../daemon/keyforkd-client", default-features = false, features = ["ed25519"], registry = "distrust" }
ed25519-dalek = "2.0.0"
sequoia-openpgp = { version = "1.17.0", default-features = false }
anyhow = "1.0.75"
thiserror = "1.0.49"
keyfork-derive-util = { workspace = true, default-features = false, features = ["ed25519"] }
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] }
ed25519-dalek = { workspace = true }
sequoia-openpgp = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
keyfork-derive-path-data = { workspace = true }

View File

@ -19,8 +19,14 @@ use sequoia_openpgp::{
Cert, Packet,
};
// TODO: this key type is actually _not_ the extended private key, so it should be renamed
// something like Prv or PrvKey.
/// The private key type used with OpenPGP.
pub type XPrvKey = SigningKey;
pub type XPrv = ExtendedPrivateKey<SigningKey>;
/// The extended private key type used with OpenPGP.
pub type XPrv = ExtendedPrivateKey<XPrvKey>;
/// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)]
@ -59,13 +65,17 @@ pub enum Error {
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Create an OpenPGP Cert with derived keys from the given derivation response, keys, and User
/// ID.
/// Create an OpenPGP Cert with private key data, with derived keys from the given derivation
/// response, keys, and User ID.
///
/// Certificates are created with a default expiration of one day, but may be configured to expire
/// later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as "15d" (15
/// days), "1m" (one month), or "2y" (two years).
///
/// # Errors
/// The function may error for any condition mentioned in [`Error`].
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.get(0) {
pub fn derive(xprv: &XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.first() {
Some(kf) if kf.for_certification() => kf,
_ => return Err(Error::NotCert),
};
@ -109,7 +119,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let cert = cert.insert_packets(vec![Packet::from(userid.clone()), binding.into()])?;
let policy = sequoia_openpgp::policy::StandardPolicy::new();
// Set certificate expiration to one day
// Set certificate expiration to configured expiration or (default) one day
let mut keypair = primary_key.clone().into_keypair()?;
let signatures =
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;

View File

@ -1,8 +1,9 @@
//!
//! Query the Keyfork Servre to derive an OpenPGP Secret Key.
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_derive_path_data::paths;
use keyfork_derive_util::DerivationPath;
use keyforkd_client::Client;
use ed25519_dalek::SigningKey;
@ -78,16 +79,17 @@ fn validate(
subkey_format: &str,
default_userid: &str,
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let index = paths::OPENPGP.inner().first().unwrap();
let path = DerivationPath::from_str(path)?;
assert_eq!(2, path.len(), "Expected path of m/{index}/account_id'");
assert!(
path.len() >= 2,
"Expected path of at least m/{index}/account_id'"
);
let given_index = path.iter().next().expect("checked .len() above");
assert_eq!(
&index, given_index,
index, given_index,
"Expected derivation path starting with m/{index}, got: {given_index}",
);
@ -118,11 +120,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.map(|kt| kt.inner().clone())
.collect::<Vec<_>>();
let cert = keyfork_derive_openpgp::derive(derived_xprv, subkeys.as_slice(), &default_userid)?;
let cert = keyfork_derive_openpgp::derive(&derived_xprv, subkeys.as_slice(), &default_userid)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
for packet in cert.into_packets() {
for packet in cert.as_tsk().into_packets() {
packet.serialize(&mut w)?;
}

View File

@ -1,10 +1,14 @@
[package]
name = "keyfork-derive-path-data"
version = "0.1.0"
version = "0.1.3"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, registry = "distrust" }
keyfork-derive-util = { workspace = true, default-features = false }
once_cell = "1.19.0"

View File

@ -2,32 +2,129 @@
#![allow(clippy::unreadable_literal)]
use std::sync::LazyLock;
use keyfork_derive_util::{DerivationIndex, DerivationPath};
/// The default derivation path for OpenPGP.
pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true);
/// All common paths for key derivation.
pub mod paths {
use super::*;
/// The default derivation path for OpenPGP.
pub static OPENPGP: LazyLock<DerivationPath> = LazyLock::new(|| {
DerivationPath::default().chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00pgp"),
true,
))
});
/// The derivation path for OpenPGP certificates used for sharding.
pub static OPENPGP_SHARD: LazyLock<DerivationPath> = LazyLock::new(|| {
DerivationPath::default()
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00pgp"),
true,
))
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"shrd"),
true,
))
});
/// The derivation path for OpenPGP certificates used for disaster recovery.
pub static OPENPGP_DISASTER_RECOVERY: LazyLock<DerivationPath> = LazyLock::new(|| {
DerivationPath::default()
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00pgp"),
true,
))
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00\x00dr"),
true,
))
});
}
/// Determine if a prefix matches and whether the next index exists.
fn prefix_matches(given: &DerivationPath, target: &DerivationPath) -> Option<DerivationIndex> {
if given.len() <= target.len() {
return None;
}
if target
.iter()
.zip(given.iter())
.all(|(left, right)| left == right)
{
given.iter().nth(target.len()).cloned()
} else {
None
}
}
/// A derivation target.
#[derive(Debug)]
#[non_exhaustive]
pub enum Target {
/// An OpenPGP key, whose account is the given index.
OpenPGP(DerivationIndex),
/// An OpenPGP key used for sharding.
OpenPGPShard(DerivationIndex),
/// An OpenPGP key used for disaster recovery.
OpenPGPDisasterRecovery(DerivationIndex),
}
impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OpenPGP(account) => {
Target::OpenPGP(account) => {
write!(f, "OpenPGP key (account {account})")
}
Target::OpenPGPShard(shard_index) => {
write!(f, "OpenPGP Shard key (shard index {shard_index})")
}
Target::OpenPGPDisasterRecovery(account) => {
write!(f, "OpenPGP Disaster Recovery key (account {account})")
}
}
}
}
macro_rules! test_match {
($var:ident, $shard:path, $target:path) => {
if let Some(index) = prefix_matches($var, &$shard) {
return Some($target(index));
}
};
}
/// Determine the closest [`Target`] for the given path. This method is intended to be used by
/// `keyforkd` to provide an optional textual prompt to what a client is attempting to derive.
pub fn guess_target(path: &DerivationPath) -> Option<Target> {
Some(match path.iter().collect::<Vec<_>>()[..] {
[t, index] if t == &OPENPGP => Target::OpenPGP(index.clone()),
_ => return None,
})
test_match!(path, paths::OPENPGP_SHARD, Target::OpenPGPShard);
test_match!(
path,
paths::OPENPGP_DISASTER_RECOVERY,
Target::OpenPGPDisasterRecovery
);
test_match!(path, paths::OPENPGP, Target::OpenPGP);
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let index = DerivationIndex::new(5312, false).unwrap();
let dr_key = paths::OPENPGP_DISASTER_RECOVERY
.clone()
.chain_push(index.clone());
match guess_target(&dr_key) {
Some(Target::OpenPGPDisasterRecovery(idx)) if idx == index => (),
bad => panic!("invalid value: {bad:?}"),
}
}
}

View File

@ -1,9 +1,12 @@
[package]
name = "keyfork-derive-util"
version = "0.1.0"
version = "0.2.2"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
@ -12,25 +15,25 @@ secp256k1 = ["k256"]
ed25519 = ["ed25519-dalek"]
[dependencies]
keyfork-mnemonic-util = { version = "0.2.0", path = "../../util/keyfork-mnemonic-util", registry = "distrust" }
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
keyfork-mnemonic = { workspace = true }
keyfork-bug = { workspace = true }
# Included in Rust
digest = "0.10.7"
sha2 = "0.10.7"
sha2 = { workspace = true }
# Rust-Crypto ecosystem, not personally audited
ripemd = "0.1.3"
hmac = { version = "0.12.1", features = ["std"] }
hmac = { workspace = true, features = ["std"] }
# Personally audited
serde = { version = "1.0.186", features = ["derive"] }
thiserror = "1.0.47"
serde = { workspace = true }
thiserror = { workspace = true }
# Optional, not personally audited
k256 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic"], optional = true }
ed25519-dalek = { version = "2.0.0", optional = true }
k256 = { workspace = true, default-features = false, features = ["std", "arithmetic"], optional = true }
ed25519-dalek = { workspace = true, optional = true }
[dev-dependencies]
hex-literal = "0.4.1"
keyfork-slip10-test-data = { version = "0.1.0", path = "../../util/keyfork-slip10-test-data", registry = "distrust" }
hex-literal = { workspace = true }
keyfork-slip10-test-data = { workspace = true }

View File

@ -23,7 +23,7 @@ performed directly on a master seed. This is how Keyforkd works internally.
```rust
use std::str::FromStr;
use keyfork_mnemonic_util::Mnemonic;
use keyfork_mnemonic::Mnemonic;
use keyfork_derive_util::{*, request::*};
fn main() -> Result<(), Box<dyn std::error::Error>> {

View File

@ -11,7 +11,7 @@
//! # Examples
//! ```rust
//! use std::str::FromStr;
//! use keyfork_mnemonic_util::Mnemonic;
//! use keyfork_mnemonic::Mnemonic;
//! use keyfork_derive_util::{*, request::*};
//! use k256::SecretKey;
//!
@ -41,9 +41,9 @@
//! }
//! ```
///
#[allow(missing_docs)]
pub mod private_key;
///
#[allow(missing_docs)]
pub mod public_key;
pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey};

View File

@ -27,6 +27,10 @@ pub enum Error {
/// The given slice was of an inappropriate size to create a Private Key.
#[error("The given slice was of an inappropriate size to create a Private Key")]
InvalidSliceError(#[from] std::array::TryFromSliceError),
/// The given data was not a valid key for the chosen key type.
#[error("The given data was not a valid key for the chosen key type")]
InvalidKey,
}
type Result<T, E = Error> = std::result::Result<T, E>;
@ -48,7 +52,7 @@ pub struct VariableLengthSeed<'a> {
}
impl<'a> VariableLengthSeed<'a> {
/// Create a new VariableLengthSeed.
/// Create a new `VariableLengthSeed`.
///
/// # Examples
/// ```rust
@ -124,10 +128,10 @@ mod serde_with {
K: PrivateKey + Clone,
{
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
let bytes: [u8; 32] = variable_len_bytes
.try_into()
.expect(bug!("unable to parse serialized private key; no support for static len"));
Ok(K::from_bytes(&bytes))
let bytes: [u8; 32] = variable_len_bytes.try_into().expect(bug!(
"unable to parse serialized private key; no support for static len"
));
Ok(K::from_bytes(&bytes).expect(bug!("could not deserialize key with invalid scalar")))
}
}
@ -148,13 +152,9 @@ where
/// Generate a new [`ExtendedPrivateKey`] from a seed, ideally from a 12-word or 24-word
/// mnemonic, but may take 16-byte seeds.
///
/// # Panics
/// The method performs unchecked `try_into()` operations on a constant-sized slice.
///
/// # Errors
/// An error may be returned if:
/// * The given seed had an incorrect length.
/// * A `HmacSha512` can't be constructed.
/// The function may return an error if the derived master key could not be parsed as a key for
/// the given algorithm (such as exceeding values on the secp256k1 curve).
///
/// # Examples
/// ```rust
@ -167,11 +167,12 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// ```
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Self {
#[allow(clippy::needless_pass_by_value)]
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Result<Self> {
Self::new_internal(seed.as_private_key())
}
fn new_internal(seed: &[u8]) -> Self {
fn new_internal(seed: &[u8]) -> Result<Self> {
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
.expect(bug!("HmacSha512 InvalidLength should be infallible"))
.chain_update(seed)
@ -179,13 +180,30 @@ where
.into_bytes();
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
Self::new_from_parts(
// Verify the master key is nonzero, hopefully avoiding side-channel attacks.
let mut has_any_nonzero = false;
// deoptimize arithmetic smartness
for byte in private_key.iter().map(std::hint::black_box) {
if *byte != 0 {
// deoptimize break
has_any_nonzero = std::hint::black_box(true);
}
}
assert!(
has_any_nonzero,
bug!("hmac function returned all-zero master key")
);
Self::from_parts(
private_key
.try_into()
.expect(bug!("KEY_SIZE / 8 did not give a 32 byte slice")),
0,
// Checked: chain_code is always the same length, hash is static size
chain_code.try_into().expect(bug!("Invalid chain code length")),
chain_code
.try_into()
.expect(bug!("Invalid chain code length")),
)
}
@ -205,13 +223,16 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code);
/// ```
pub fn new_from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self {
Self {
private_key: K::from_bytes(key),
depth,
chain_code,
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
match K::from_bytes(key) {
Ok(key) => Ok(Self {
private_key: key,
depth,
chain_code,
}),
Err(_) => Err(Error::InvalidKey),
}
}
@ -225,12 +246,15 @@ where
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key));
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key)?);
/// # Ok(())
/// # }
/// ```
pub fn private_key(&self) -> &K {
&self.private_key
@ -255,14 +279,14 @@ where
/// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27,
/// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251
/// # ];
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let xpub = xprv.extended_public_key();
/// assert_eq!(known_key, xpub.public_key().to_bytes());
/// # Ok(())
/// # }
/// ```
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
ExtendedPublicKey::new_from_parts(self.public_key(), self.depth, self.chain_code)
ExtendedPublicKey::from_parts(self.public_key(), self.depth, self.chain_code)
}
/// Return a public key for the current [`PrivateKey`].
@ -279,7 +303,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let pubkey = xprv.public_key();
/// # Ok(())
/// # }
@ -297,12 +321,15 @@ where
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
/// assert_eq!(xprv.depth(), 4);
/// # Ok(())
/// # }
/// ```
pub fn depth(&self) -> u8 {
self.depth
@ -317,12 +344,15 @@ where
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// let xprv = ExtendedPrivateKey::<PrivateKey>::from_parts(key, 4, *chain_code)?;
/// assert_eq!(chain_code, &xprv.chain_code());
/// # Ok(())
/// # }
/// ```
pub fn chain_code(&self) -> [u8; 32] {
self.chain_code
@ -344,7 +374,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let path = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
@ -390,7 +420,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed)?;
/// let bip44_wallet = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)

View File

@ -11,8 +11,8 @@ const KEY_SIZE: usize = 256;
/// Errors associated with creating or deriving Extended Public Keys.
#[derive(Error, Clone, Debug)]
pub enum Error {
/// BIP-0032 does not support deriving public keys from hardened private keys.
#[error("Public keys may not be derived when hardened")]
/// BIP-0032 does not support hardened public key derivation from parent public keys.
#[error("Hardened child public keys may not be derived from parent public keys")]
HardenedIndex,
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
@ -60,11 +60,11 @@ where
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let pubkey = PublicKey::from_bytes(key);
/// let xpub = ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// let xpub = ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// # Ok(())
/// # }
/// ```
pub fn new_from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
pub fn from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
Self {
public_key,
depth,
@ -86,7 +86,7 @@ where
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// let pubkey = xpub.public_key();
/// # Ok(())
/// # }
@ -121,7 +121,7 @@ where
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// # ExtendedPublicKey::<PublicKey>::from_parts(pubkey, 0, *chain_code);
/// let index = DerivationIndex::new(0, false)?;
/// let child = xpub.derive_child(&index)?;
/// # Ok(())

View File

@ -117,7 +117,7 @@ mod tests {
use std::str::FromStr;
#[test]
#[should_panic]
#[should_panic(expected = "IndexTooLarge")]
fn fails_on_high_index() {
DerivationIndex::new(0x8000_0001, false).unwrap();
}
@ -163,7 +163,7 @@ mod tests {
}
#[test]
#[should_panic]
#[should_panic(expected = "IndexTooLarge")]
fn from_str_fails_on_high_index() {
DerivationIndex::from_str(&0x8000_0001u32.to_string()).unwrap();
}

View File

@ -1,4 +1,4 @@
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
#![allow(clippy::module_name_repetitions)]
#![doc = include_str!("../README.md")]
pub mod extended_key;
@ -17,7 +17,10 @@ pub mod public_key;
mod tests;
#[doc(inline)]
pub use crate::extended_key::{private_key::{ExtendedPrivateKey, Error as XPrvError, VariableLengthSeed}, public_key::{ExtendedPublicKey, Error as XPubError}};
pub use crate::extended_key::{
private_key::{Error as XPrvError, ExtendedPrivateKey, VariableLengthSeed},
public_key::{Error as XPubError, ExtendedPublicKey},
};
pub use crate::{
index::{DerivationIndex, Error as IndexError},

View File

@ -11,7 +11,7 @@ pub enum Error {
/// The path could not be parsed due to a bad prefix. Paths must be in the format:
///
/// m [/ index [']]+
/// `m [/ index [']]+`
///
/// The prefix for the path must be `m`, and all indices must be integers between 0 and
/// 2^31.
@ -35,8 +35,8 @@ impl DerivationPath {
self.path.iter()
}
/// The amount of segments in the DerivationPath. For consistency, a [`usize`] is returned, but
/// BIP-0032 dictates that the depth should be no larger than `255`, [`u8::MAX`].
/// The amount of segments in the [`DerivationPath`]. For consistency, a [`usize`] is returned,
/// but BIP-0032 dictates that the depth should be no larger than `255`, [`u8::MAX`].
pub fn len(&self) -> usize {
self.path.len()
}
@ -134,7 +134,7 @@ mod tests {
use std::str::FromStr;
#[test]
#[should_panic]
#[should_panic(expected = "UnknownPathPrefix")]
fn requires_master_path() {
DerivationPath::from_str("1234/5678'").unwrap();
}

View File

@ -2,8 +2,6 @@ use crate::PublicKey;
use thiserror::Error;
use keyfork_bug::bug;
pub(crate) type PrivateKeyBytes = [u8; 32];
/// Functions required to use an `ExtendedPrivateKey`.
@ -26,7 +24,7 @@ pub trait PrivateKey: Sized {
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// ```
fn from_bytes(b: &PrivateKeyBytes) -> Self;
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err>;
/// Convert a &Self to bytes.
///
@ -38,7 +36,7 @@ pub trait PrivateKey: Sized {
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// assert_eq!(key_data, &private_key.to_bytes());
/// ```
fn to_bytes(&self) -> PrivateKeyBytes;
@ -73,7 +71,7 @@ pub trait PrivateKey: Sized {
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// let public_key = private_key.public_key();
/// ```
fn public_key(&self) -> Self::PublicKey;
@ -85,7 +83,7 @@ pub trait PrivateKey: Sized {
/// # Errors
///
/// An error may be returned if:
/// * A nonzero `other` is provided.
/// * An all-zero `other` is provided.
/// * An error specific to the given algorithm was encountered.
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
@ -102,6 +100,10 @@ pub enum PrivateKeyError {
/// For the given algorithm, the private key must be nonzero.
#[error("The provided private key must be nonzero, but is not")]
NonZero,
/// A scalar could not be constructed for the given algorithm.
#[error("A scalar could not be constructed for the given algorithm")]
InvalidScalar,
}
#[cfg(feature = "secp256k1")]
@ -116,8 +118,8 @@ impl PrivateKey for k256::SecretKey {
"Bitcoin seed"
}
fn from_bytes(b: &PrivateKeyBytes) -> Self {
Self::from_slice(b).expect(bug!("Invalid private key bytes"))
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
Self::from_slice(b).map_err(|_| PrivateKeyError::InvalidScalar)
}
fn to_bytes(&self) -> PrivateKeyBytes {
@ -130,20 +132,19 @@ impl PrivateKey for k256::SecretKey {
}
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
if other.iter().all(|n| n == &0) {
return Err(PrivateKeyError::NonZero);
}
let other = *other;
// Checked: See above nonzero check
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
.expect(bug!("Should have been able to get a NonZeroScalar"));
use k256::elliptic_curve::ScalarPrimitive;
use k256::{Scalar, Secp256k1};
// Construct a scalar from bytes
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(other.into());
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
let scalar = scalar.ok_or(PrivateKeyError::InvalidScalar)?;
let scalar = Scalar::from(scalar);
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
Ok(
Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
.map(Into::into)
.expect(bug!("Should be able to make Key")),
)
let nonzero_scalar = Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
.ok_or(PrivateKeyError::NonZero)?;
Ok(Self::from(nonzero_scalar))
}
}
@ -156,8 +157,8 @@ impl PrivateKey for ed25519_dalek::SigningKey {
"ed25519 seed"
}
fn from_bytes(b: &PrivateKeyBytes) -> Self {
Self::from_bytes(b)
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
Ok(Self::from_bytes(b))
}
fn to_bytes(&self) -> PrivateKeyBytes {
@ -180,7 +181,8 @@ impl PrivateKey for ed25519_dalek::SigningKey {
use crate::public_key::TestPublicKey;
#[doc(hidden)]
/// A private key that can be used for testing purposes. Does not utilize any significant
/// cryptographic operations.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TestPrivateKey {
key: [u8; 32],
@ -200,10 +202,8 @@ impl PrivateKey for TestPrivateKey {
type PublicKey = TestPublicKey;
type Err = PrivateKeyError;
fn from_bytes(b: &PrivateKeyBytes) -> Self {
Self {
key: *b
}
fn from_bytes(b: &PrivateKeyBytes) -> Result<Self, Self::Err> {
Ok(Self { key: *b })
}
fn to_bytes(&self) -> PrivateKeyBytes {

View File

@ -30,7 +30,7 @@ pub trait PublicKey: Sized {
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// let public_key_bytes = private_key.public_key().to_bytes();
/// ```
fn to_bytes(&self) -> PublicKeyBytes;
@ -42,7 +42,7 @@ pub trait PublicKey: Sized {
/// # Errors
///
/// An error may be returned if:
/// * A nonzero `other` is provided.
/// * An all-zero `other` is provided.
/// * An error specific to the given algorithm was encountered.
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
@ -56,7 +56,7 @@ pub trait PublicKey: Sized {
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// let private_key = OurPrivateKey::from_bytes(key_data).unwrap();
/// let fingerprint = private_key.public_key().fingerprint();
/// ```
fn fingerprint(&self) -> [u8; 4] {
@ -77,6 +77,10 @@ pub enum PublicKeyError {
#[error("The provided public key must be nonzero, but is not")]
NonZero,
/// A scalar could not be constructed for the given algorithm.
#[error("A scalar could not be constructed for the given algorithm")]
InvalidScalar,
/// Public key derivation is unsupported for this algorithm.
#[error("Public key derivation is unsupported for this algorithm")]
DerivationUnsupported,
@ -85,7 +89,7 @@ pub enum PublicKeyError {
#[cfg(feature = "secp256k1")]
use k256::{
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
AffinePoint, NonZeroScalar,
AffinePoint,
};
#[cfg(feature = "secp256k1")]
@ -105,14 +109,16 @@ impl PublicKey for k256::PublicKey {
}
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
if other.iter().all(|n| n == &0) {
return Err(PublicKeyError::NonZero);
}
// Checked: See above
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
.expect(bug!("Should have been able to get a NonZeroScalar"));
use k256::elliptic_curve::ScalarPrimitive;
use k256::{Scalar, Secp256k1};
let point = self.to_projective() + (AffinePoint::generator() * *scalar);
// Construct a scalar from bytes
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(&other.into());
let scalar = Option::<ScalarPrimitive<Secp256k1>>::from(scalar);
let scalar = scalar.ok_or(PublicKeyError::InvalidScalar)?;
let scalar = Scalar::from(scalar);
let point = self.to_projective() + (AffinePoint::generator() * scalar);
Ok(Self::from_affine(point.into())
.expect(bug!("Could not from_affine after scalar arithmetic")))
}
@ -142,14 +148,15 @@ impl PublicKey for VerifyingKey {
}
}
#[doc(hidden)]
/// A public key that can be used for testing purposes. Does not utilize any significant
/// cryptographic operations.
#[derive(Clone)]
pub struct TestPublicKey {
pub(crate) key: [u8; 33],
}
impl TestPublicKey {
#[doc(hidden)]
/// Create a new [`TestPublicKey`] from the given bytes.
#[allow(dead_code)]
pub fn from_bytes(b: &[u8]) -> Self {
Self {

View File

@ -24,7 +24,7 @@ use crate::{
DerivationPath, ExtendedPrivateKey,
};
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
use keyfork_mnemonic::{Mnemonic, MnemonicGenerationError};
use serde::{Deserialize, Serialize};
/// An error encountered while deriving a key.
@ -57,7 +57,7 @@ pub enum DerivationAlgorithm {
#[allow(missing_docs)]
Secp256k1,
#[doc(hidden)]
Internal,
TestAlgorithm,
}
impl DerivationAlgorithm {
@ -70,7 +70,7 @@ impl DerivationAlgorithm {
match self {
#[cfg(feature = "ed25519")]
Self::Ed25519 => {
let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed);
let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed)?;
let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv(
self.clone(),
@ -79,15 +79,15 @@ impl DerivationAlgorithm {
}
#[cfg(feature = "secp256k1")]
Self::Secp256k1 => {
let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed);
let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed)?;
let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv(
self.clone(),
&derived_key,
))
}
Self::Internal => {
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed);
Self::TestAlgorithm => {
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed)?;
let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv(
self.clone(),
@ -120,7 +120,7 @@ pub trait AsAlgorithm: PrivateKey {
impl AsAlgorithm for TestPrivateKey {
fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::Internal
DerivationAlgorithm::TestAlgorithm
}
}
@ -144,7 +144,7 @@ impl DerivationRequest {
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::Internal;
/// # DerivationAlgorithm::TestAlgorithm;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
@ -169,7 +169,7 @@ impl DerivationRequest {
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::Internal;
/// # DerivationAlgorithm::TestAlgorithm;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
@ -194,12 +194,12 @@ impl DerivationRequest {
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mnemonic: keyfork_mnemonic_util::Mnemonic = //
/// # keyfork_mnemonic_util::Mnemonic::from_entropy(
/// let mnemonic: keyfork_mnemonic::Mnemonic = //
/// # keyfork_mnemonic::Mnemonic::from_entropy(
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
/// # )?;
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::Internal;
/// # DerivationAlgorithm::TestAlgorithm;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
@ -228,7 +228,7 @@ impl DerivationRequest {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::Internal;
/// # DerivationAlgorithm::TestAlgorithm;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
@ -300,11 +300,9 @@ mod secp256k1 {
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Secp256k1 => Ok(Self::new_from_parts(
&value.data,
value.depth,
value.chain_code,
)),
DerivationAlgorithm::Secp256k1 => {
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into)
}
_ => Err(Self::Error::Algorithm),
}
}
@ -335,11 +333,9 @@ mod ed25519 {
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Ed25519 => Ok(Self::new_from_parts(
&value.data,
value.depth,
value.chain_code,
)),
DerivationAlgorithm::Ed25519 => {
Self::from_parts(&value.data, value.depth, value.chain_code).map_err(Into::into)
}
_ => Err(Self::Error::Algorithm),
}
}

View File

@ -13,10 +13,7 @@ use keyfork_slip10_test_data::{test_data, Test};
fn secp256k1() {
use k256::SecretKey;
let tests = test_data()
.unwrap()
.remove(&"secp256k1".to_string())
.unwrap();
let tests = test_data().unwrap().remove("secp256k1").unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -31,7 +28,7 @@ fn secp256k1() {
// Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed);
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed).unwrap();
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
derived_key.chain_code().as_slice(),
@ -62,7 +59,7 @@ fn secp256k1() {
fn ed25519() {
use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
let tests = test_data().unwrap().remove("ed25519").unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -77,7 +74,7 @@ fn ed25519() {
// Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed);
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed).unwrap();
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
derived_key.chain_code().as_slice(),
@ -105,24 +102,24 @@ fn ed25519() {
#[cfg(feature = "ed25519")]
#[test]
#[should_panic]
#[should_panic(expected = "HardenedDerivationRequired")]
fn panics_with_unhardened_derivation() {
use ed25519_dalek::SigningKey;
let seed = hex!("000102030405060708090a0b0c0d0e0f");
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
xkey.derive_path(&DerivationPath::from_str("m/0").unwrap())
.unwrap();
}
#[cfg(feature = "ed25519")]
#[test]
#[should_panic]
#[should_panic(expected = "Depth")]
fn panics_at_depth() {
use ed25519_dalek::SigningKey;
let seed = hex!("000102030405060708090a0b0c0d0e0f");
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
for i in 0..=u32::from(u8::MAX) {
xkey = xkey
.derive_child(&DerivationIndex::new(i, true).unwrap())

View File

@ -1,9 +1,12 @@
[package]
name = "keyfork-shard"
version = "0.1.0"
version = "0.3.4"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
@ -14,26 +17,26 @@ openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "de
qrcode = ["keyfork-qrcode"]
[dependencies]
keyfork-bug = { version = "0.1.0", path = "../util/keyfork-bug", registry = "distrust" }
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"], registry = "distrust" }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", optional = true, default-features = false, registry = "distrust" }
smex = { version = "0.1.0", path = "../util/smex", registry = "distrust" }
sharks = "0.5.0"
thiserror = "1.0.50"
keyfork-bug = { workspace = true }
keyfork-prompt = { workspace = true, default-features = false, features = ["mnemonic"] }
keyfork-qrcode = { workspace = true, optional = true, default-features = false }
smex = { workspace = true }
thiserror = { workspace = true }
# Remote operator mode
keyfork-mnemonic-util = { version = "0.2.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
keyfork-mnemonic = { workspace = true }
x25519-dalek = { version = "2.0.0", features = ["getrandom"] }
aes-gcm = { version = "0.10.3", features = ["std"] }
hkdf = { version = "0.12.4", features = ["std"] }
sha2 = "0.10.8"
sha2 = { workspace = true }
# OpenPGP
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp", default-features = false, registry = "distrust" }
anyhow = { version = "1.0.79", optional = true }
keyfork-derive-openpgp = { workspace = true, default-features = false }
anyhow = { workspace = true, optional = true }
card-backend = { version = "0.2.0", optional = true }
card-backend-pcsc = { version = "0.5.0", optional = true }
openpgp-card-sequoia = { version = "0.2.0", optional = true, default-features = false }
openpgp-card = { version = "0.4.0", optional = true }
sequoia-openpgp = { version = "1.17.0", optional = true, default-features = false }
card-backend-pcsc = { workspace = true, optional = true }
openpgp-card-sequoia = { workspace = true, optional = true }
openpgp-card = { workspace = true, optional = true }
sequoia-openpgp = { workspace = true, optional = true }
base64 = { workspace = true }
blahaj = "0.6.0"

View File

@ -1,4 +1,4 @@
//!
//! Combine OpenPGP shards and output the hex-encoded secret.
use std::{
env,
@ -7,7 +7,7 @@ use std::{
process::ExitCode,
};
use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_prompt::default_handler;
use keyfork_shard::{openpgp::OpenPGP, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -32,10 +32,14 @@ fn run() -> Result<()> {
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let openpgp = OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
let openpgp = OpenPGP;
let prompt_handler = default_handler()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file, prompt_handler)?;
let bytes = openpgp.decrypt_all_shards_to_secret(
key_discovery.as_deref(),
messages_file,
prompt_handler,
)?;
print!("{}", smex::encode(bytes));
Ok(())

View File

@ -1,4 +1,4 @@
//!
//! Decrypt a single OpenPGP shard and encapsulate it for remote transport.
use std::{
env,
@ -7,8 +7,8 @@ use std::{
process::ExitCode,
};
use keyfork_prompt::{DefaultTerminal, default_terminal};
use keyfork_shard::{Format, openpgp::OpenPGP};
use keyfork_prompt::default_handler;
use keyfork_shard::{openpgp::OpenPGP, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -32,10 +32,14 @@ fn run() -> Result<()> {
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let openpgp = OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
let openpgp = OpenPGP;
let prompt_handler = default_handler()?;
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file, prompt_handler)?;
openpgp.decrypt_one_shard_for_transport(
key_discovery.as_deref(),
messages_file,
prompt_handler,
)?;
Ok(())
}

View File

@ -1,9 +1,6 @@
//!
//! Combine OpenPGP shards using remote transport and output the hex-encoded secret.
use std::{
env,
process::ExitCode,
};
use std::{env, process::ExitCode};
use keyfork_shard::remote_decrypt;
@ -16,7 +13,7 @@ fn run() -> Result<()> {
match args.as_slice() {
[] => (),
_ => panic!("Usage: {program_name}"),
};
}
let mut bytes = vec![];
remote_decrypt(&mut bytes)?;

View File

@ -1,9 +1,8 @@
//!
//! Split a hex-encoded secret into OpenPGP shards
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_prompt::terminal::DefaultTerminal;
use keyfork_shard::{Format, openpgp::OpenPGP};
use keyfork_shard::{openpgp::OpenPGP, Format};
#[derive(Clone, Debug)]
enum Error {
@ -51,9 +50,15 @@ fn run() -> Result<()> {
smex::decode(line?)?
};
let openpgp = OpenPGP::<DefaultTerminal>::new();
let openpgp = OpenPGP;
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
openpgp.shard_and_encrypt(
threshold,
max,
&input,
key_discovery.as_path(),
std::io::stdout(),
)?;
Ok(())
}

View File

@ -1,28 +1,77 @@
#![doc = include_str!("../README.md")]
#![allow(clippy::expect_fun_call)]
use std::{
io::{stdin, stdout, Read, Write},
sync::{Arc, Mutex},
io::{Read, Write},
rc::Rc,
str::FromStr,
sync::{LazyLock, Mutex},
};
use aes_gcm::{
aead::{consts::U12, Aead, AeadCore, OsRng},
aead::{consts::U12, Aead},
Aes256Gcm, KeyInit, Nonce,
};
use base64::prelude::{Engine, BASE64_STANDARD};
use blahaj::{Share, Sharks};
use hkdf::Hkdf;
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{
validators::{mnemonic::MnemonicSetValidator, Validator},
Message as PromptMessage, PromptHandler, Terminal,
prompt_validated_wordlist,
validators::{
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
Validator,
},
Message as PromptMessage, PromptHandler, YesNo,
};
use sha2::Sha256;
use sharks::{Share, Sharks};
use sha2::{Digest, Sha256};
use x25519_dalek::{EphemeralSecret, PublicKey};
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
const ENC_LEN: u8 = 4 * 16;
const PLAINTEXT_LENGTH: u8 = 32 // shard
+ 1 // index
+ 1 // threshold
+ 1 // version
+ 1; // length;
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."),
}
}
}
fn calculate_checksum(slice: &[u8]) -> Vec<u8> {
// generate a verification checksum
// this checksum should be expensive to calculate
let mut payload = vec![];
for _ in 0..1_000_000 {
payload.extend(slice);
let mut hasher = Sha256::new();
hasher.update(&payload);
let result = hasher.finalize();
payload.clear();
payload.extend(result);
}
payload
}
#[cfg(feature = "openpgp")]
pub mod openpgp;
@ -42,9 +91,10 @@ pub trait KeyDiscovery<F: Format + ?Sized> {
/// # Errors
/// The method may return an error if private keys could not be loaded from the given
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
/// `PrivateKeyData` type of the asssociated format should be either `()` (if the keys may
/// never exist on-system) or an empty container (such as an empty Vec); in either case, this
/// method _must not_ return an error if keys are accessible but can't be transferred into
/// memory.
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
}
@ -71,8 +121,8 @@ pub trait Format {
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
/// will use the same key_data for both, ensuring an iteration of this method will match with
/// iterations in methods called later.
/// will use the same `key_data` for both, ensuring an iteration of this method will match
/// with iterations in methods called later.
///
/// # Errors
/// The method may return an error if encryption to any of the public keys fails.
@ -127,10 +177,10 @@ pub trait Format {
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_messages: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
) -> 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
/// combine secrets.
///
@ -141,9 +191,43 @@ pub trait Format {
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
) -> 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.
///
/// # Errors
@ -153,7 +237,7 @@ pub trait Format {
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: impl PromptHandler,
prompt: Box<dyn PromptHandler>,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
@ -162,7 +246,7 @@ pub trait Format {
let (shares, threshold) = self.decrypt_all_shards(
private_keys,
&encrypted_messages,
Arc::new(Mutex::new(prompt)),
Rc::new(Mutex::new(prompt)),
)?;
let secret = Sharks(threshold)
@ -179,13 +263,14 @@ pub trait Format {
/// The method may return an error if a share can't be decrypted. The method will not return an
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
/// QR code; instead, a mnemonic prompt will be used.
#[allow(clippy::too_many_lines)]
fn decrypt_one_shard_for_transport(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
prompt: impl PromptHandler,
prompt: Box<dyn PromptHandler>,
) -> Result<(), Box<dyn std::error::Error>> {
let prompt = Arc::new(Mutex::new(prompt));
let prompt = Rc::new(Mutex::new(prompt));
// parse input
let private_keys = private_key_discovery
@ -194,7 +279,6 @@ pub trait Format {
let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera
@ -204,58 +288,85 @@ pub trait Format {
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
{
let decoded_data = smex::decode(&hex)?;
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
} else {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
};
loop {
if let Ok(Some(qrcode_content)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
{
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
let data: [u8; 32] = decoded_data.try_into().map_err(|_| InvalidData)?;
let checksum = calculate_checksum(&data);
let small_sum = &checksum[..8];
let small_mnemonic = Mnemonic::from_raw_bytes(small_sum);
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
let question =
format!("Do these words match the expected words? {small_mnemonic}");
let response = keyfork_prompt::prompt_choice(
&mut **prompt,
&question,
&[YesNo::No, YesNo::Yes],
)?;
if response == YesNo::No {
prompt.prompt_message(PromptMessage::Text(String::from(
"Could not establish secure channel, exiting.",
)))?;
std::process::exit(1);
}
pubkey_data = Some(data);
break;
} else {
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
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [9, 24],
};
let [nonce_mnemonic, pubkey_mnemonic] = prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let nonce = nonce_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
(nonce, pubkey)
}
let their_pubkey = if let Some(pubkey) = pubkey_data {
pubkey
} else {
let validator = MnemonicValidator {
word_length: Some(WordLength::Count(24)),
};
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
prompt_validated_wordlist::<English, _>(
&mut **prompt,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
};
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_pubkey))
.to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
assert!(
shared_secret.was_contributory(),
bug!("shared secret might be insecure")
);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut shared_key_data = [0u8; 256 / 8];
hkdf.expand(b"key", &mut shared_key_data)?;
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
let mut nonce_data = [0u8; 12];
hkdf.expand(b"nonce", &mut nonce_data)?;
let nonce = Nonce::<U12>::from_slice(&nonce_data);
// decrypt a single shard and create the payload
let (share, threshold) =
@ -264,49 +375,46 @@ pub trait Format {
payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold);
assert!(
payload.len() <= ENC_LEN as usize,
"invalid share length (too long, max {ENC_LEN} bytes)"
payload.len() < PLAINTEXT_LENGTH as usize,
"invalid share length (too long, must be less than {PLAINTEXT_LENGTH} bytes)"
);
// encrypt data
let nonce = Nonce::<U12>::from_slice(&nonce);
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
// convert data to a static-size payload
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
// convert plaintext to static-size payload
#[allow(clippy::assertions_on_constants)]
{
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
assert!(PLAINTEXT_LENGTH < u8::MAX, "length byte can be u8");
}
#[allow(clippy::cast_possible_truncation)]
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
assert!(
payload_bytes.len() < out_bytes.len(),
"encrypted payload larger than acceptable limit"
// NOTE: Previous versions of Keyfork Shard would modify the padding bytes to avoid
// duplicate mnemonic words. This version does not include that, and instead uses a
// repeated length byte.
let mut plaintext_bytes = [u8::try_from(payload.len()).expect(bug!(
"previously asserted length must be < {PLAINTEXT_LENGTH}",
PLAINTEXT_LENGTH = PLAINTEXT_LENGTH
)); PLAINTEXT_LENGTH as usize];
plaintext_bytes[..payload.len()].clone_from_slice(&payload);
// encrypt data
let encrypted_bytes = shared_key.encrypt(nonce, plaintext_bytes.as_slice())?;
assert_eq!(
encrypted_bytes.len(),
ENCRYPTED_LENGTH as usize,
bug!("encrypted bytes size != expected len"),
);
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
mnemonic_bytes.copy_from_slice(&encrypted_bytes);
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
// difficulty when entering in prompts manually, as one's place could be lost due to
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
// but not including the last byte.
#[allow(clippy::cast_possible_truncation)]
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
.iter_mut()
.enumerate()
{
*byte = (i % u8::MAX as usize) as u8;
}
// safety: size of out_bytes is constant and always % 4 == 0
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
let payload_mnemonic = Mnemonic::from_array(mnemonic_bytes);
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
@ -359,11 +467,11 @@ pub trait Format {
"must have less than u8::MAX public keys"
);
assert_eq!(
max,
public_keys.len() as u8,
max as usize,
public_keys.len(),
"max must be equal to amount of public keys"
);
let max = public_keys.len() as u8;
let max = u8::try_from(public_keys.len()).expect(bug!("invalid max: {max}", max = max));
assert!(max >= threshold, "threshold must not exceed max keys");
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
@ -401,13 +509,17 @@ pub struct InvalidData;
/// 1 byte: Version
/// 1 byte: Threshold
/// Data: &[u8]
pub(crate) const HUNK_VERSION: u8 = 1;
pub(crate) const HUNK_VERSION: u8 = 2;
pub(crate) const HUNK_OFFSET: usize = 2;
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_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
/// shares, and combine them.
@ -421,8 +533,9 @@ const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry
/// # Panics
/// The function may panic if it is given payloads generated using a version of Keyfork that is
/// incompatible with the currently running version.
#[allow(clippy::too_many_lines)]
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 shares = vec![];
@ -432,36 +545,40 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
let our_key = EphemeralSecret::random();
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = nonce_mnemonic.to_bytes();
qrcode_data.extend(key_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
let qrcode_data = key_mnemonic.to_bytes();
if let Ok(qrcode) = qrencode(
&BASE64_STANDARD.encode(qrcode_data),
ErrorCorrection::Highest,
) {
let checksum = calculate_checksum(key_mnemonic.as_bytes());
let small_sum = &checksum[..8];
let small_mnemonic = Mnemonic::from_raw_bytes(small_sum);
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code to only shardholder {iter}. ",
"Nobody else should scan this QR code."
"QR code #{iter} will be displayed after this prompt. ",
"Send the QR code to the next shardholder. ",
"Only the next shardholder should scan the QR code. ",
),
iter = iter
iter = iter,
)))?;
pm.prompt_message(PromptMessage::Data(qrcode))?;
pm.prompt_message(PromptMessage::Text(format!(
"The following should be sent to verify the QR code: {small_mnemonic}"
)))?;
}
}
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"Upon request, these words should be sent to shardholder {iter}: ",
"{nonce_mnemonic} {key_mnemonic}"
"Upon request, these words should be sent to the shardholder: ",
"{key_mnemonic}"
),
iter = iter,
nonce_mnemonic = nonce_mnemonic,
key_mnemonic = key_mnemonic,
)))?;
@ -471,62 +588,92 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
{
let decoded_data = smex::decode(&hex)?;
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
let _ = payload_data.insert(decoded_data[32..].to_vec());
} else {
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
};
loop {
if let Ok(Some(qrcode_content)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(*QRCODE_TIMEOUT), 0)
{
let decoded_data = BASE64_STANDARD
.decode(qrcode_content)
.expect(bug!("qrcode should contain base64 encoded data"));
assert_eq!(
decoded_data.len(),
// Include length of public key
ENCRYPTED_LENGTH as usize + 32,
bug!("invalid payload data")
);
let _ =
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) {
(Some(pubkey), Some(payload)) => (pubkey, payload),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [24, 48],
};
let (pubkey, payload) = if let Some((pubkey, payload)) = pubkey_data.zip(payload_data) {
(pubkey, payload)
} else {
let validator = MnemonicSetValidator {
word_lengths: [24, 39],
};
let [pubkey_mnemonic, payload_mnemonic] = pm
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let payload = payload_mnemonic.to_bytes();
(pubkey, payload)
}
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
&mut *pm,
QRCODE_COULDNT_READ,
3,
&*validator.to_fn(),
)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let payload = payload_mnemonic.to_bytes();
(pubkey, payload)
};
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
assert_eq!(
payload.len(),
ENCRYPTED_LENGTH as usize,
bug!("invalid payload data")
);
let payload =
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey));
assert!(
shared_secret.was_contributory(),
bug!("shared secret might be insecure")
);
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut shared_key_data = [0u8; 256 / 8];
hkdf.expand(b"key", &mut shared_key_data)?;
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
let mut nonce_data = [0u8; 12];
hkdf.expand(b"nonce", &mut nonce_data)?;
let nonce = Nonce::<U12>::from_slice(&nonce_data);
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
match &mut iter_count {
Some(n) => {
// Must be > 0 to start loop, can't go lower
*n -= 1;
}
None => {
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
threshold = payload[1];
let _ = iter_count.insert(threshold - 1);
}
if let Some(n) = &mut iter_count {
// Must be > 0 to start loop, can't go lower
*n -= 1;
} else {
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
threshold = payload[1];
let _ = iter_count.insert(threshold - 1);
}
shares.push(payload[HUNK_OFFSET..].to_vec());
let payload_len = payload.last().expect(bug!("payload should not be empty"));
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
}
let shares = shares

View File

@ -1,16 +1,15 @@
//! OpenPGP Shard functionality.
#![allow(clippy::expect_fun_call)]
use std::{
collections::HashMap,
io::{Read, Write},
marker::PhantomData,
path::Path,
rc::Rc,
str::FromStr,
sync::{Arc, Mutex},
sync::Mutex,
};
use blahaj::Share;
use keyfork_bug::bug;
use keyfork_derive_openpgp::{
derive_util::{DerivationPath, VariableLengthSeed},
@ -34,7 +33,6 @@ use openpgp::{
KeyID, PacketPile,
};
pub use sequoia_openpgp as openpgp;
use sharks::Share;
mod keyring;
use keyring::Keyring;
@ -77,6 +75,10 @@ pub enum Error {
/// An IO error occurred.
#[error("IO error: {0}")]
Io(#[source] std::io::Error),
/// No valid keys were found for the given recipient.
#[error("No valid keys were found for the recipient {0}")]
NoValidKeys(KeyID),
}
#[allow(missing_docs)]
@ -90,7 +92,7 @@ pub struct EncryptedMessage {
}
impl EncryptedMessage {
/// Create a new EncryptedMessage from known parts.
/// Create a new [`EncryptedMessage`] from known parts.
pub fn new(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self {
Self {
pkesks: std::mem::take(pkesks),
@ -156,7 +158,7 @@ impl EncryptedMessage {
/// Decrypt the message with a Sequoia policy and decryptor.
///
/// This method creates a container containing the packets and passes the serialized container
/// to a DecryptorBuilder, which is used to decrypt the message.
/// to a `DecryptorBuilder`, which is used to decrypt the message.
///
/// # Errors
/// The method may return an error if it is unable to rebuild the message to decrypt or if it
@ -181,52 +183,69 @@ impl EncryptedMessage {
}
}
///
pub struct OpenPGP<P: PromptHandler> {
p: PhantomData<P>,
}
/// Encoding and decoding shards using OpenPGP.
pub struct OpenPGP;
impl<P: PromptHandler> OpenPGP<P> {
#[allow(clippy::new_without_default, missing_docs)]
pub fn new() -> Self {
Self { p: PhantomData }
}
}
impl<P: PromptHandler> OpenPGP<P> {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
/// from a file, or from files one level deep in a directory.
impl OpenPGP {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them.
///
/// Certificates are read from a file, or from files one level deep in a directory.
/// Certificates with duplicated fingerprints will be discarded.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
/// The function may return an error if it is unable to read the directory or if Sequoia is
/// unable to load certificates from the file.
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref();
let mut pubkeys = std::collections::HashSet::new();
let mut certs = HashMap::new();
if path.is_file() {
let mut vec = vec![];
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
vec.push(cert.map_err(Error::Sequoia)?);
for maybe_cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
let cert = maybe_cert.map_err(Error::Sequoia)?;
let certfp = cert.fingerprint();
for key in cert.keys() {
let fp = key.fingerprint();
if pubkeys.contains(&fp) {
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
}
pubkeys.insert(fp);
}
certs.insert(certfp, cert);
}
Ok(vec)
} else {
let mut vec = vec![];
for entry in path
.read_dir()
.map_err(Error::Io)?
.filter_map(Result::ok)
.filter(|p| p.path().is_file())
{
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
let cert = Cert::from_file(entry.path()).map_err(Error::Sequoia)?;
let certfp = cert.fingerprint();
for key in cert.keys() {
let fp = key.fingerprint();
if pubkeys.contains(&fp) {
eprintln!("Received duplicate key: {fp} in public key: {certfp}");
}
pubkeys.insert(fp);
}
certs.insert(certfp, cert);
}
Ok(vec)
}
for cert in certs.values() {
let policy = StandardPolicy::new();
let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?;
if get_encryption_keys(&valid_cert).next().is_none() {
return Err(Error::NoValidKeys(valid_cert.keyid()));
}
}
Ok(certs.into_values().collect())
}
}
const METADATA_MESSAGE_MISSING: &str = "Metadata message was not found in parsed packets";
impl<P: PromptHandler> Format for OpenPGP<P> {
impl Format for OpenPGP {
type Error = Error;
type PublicKey = Cert;
type PrivateKeyData = Vec<Cert>;
@ -240,10 +259,11 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
let userid = UserID::from("keyfork-sss");
let path = DerivationPath::from_str("m/7366512'/0'").expect(bug!("valid derivation path"));
let xprv = XPrv::new(seed)
.expect(bug!("could not create XPrv from key"))
.derive_path(&path)
.expect(bug!("valid derivation"));
keyfork_derive_openpgp::derive(
xprv,
&xprv,
&[KeyFlags::empty().set_certification().set_signing()],
&userid,
)
@ -422,14 +442,14 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
// Be as liberal as possible when decrypting.
// We don't want to invalidate someone's keys just because the old sig expired.
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 keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
let mut manager = SmartcardManager::new(prompt.clone());
let mut encrypted_messages = encrypted_data.iter();
@ -460,9 +480,9 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
let left_from_threshold = threshold as usize - decrypted_messages.len();
if left_from_threshold > 0 {
#[allow(clippy::cast_possible_truncation)]
let new_messages = decrypt_with_manager(
left_from_threshold as u8,
u8::try_from(left_from_threshold)
.expect(bug!("threshold too large: {}", left_from_threshold)),
&mut messages,
&certs,
&policy,
@ -483,12 +503,12 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
prompt: Arc<Mutex<impl PromptHandler>>,
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
) -> std::result::Result<(Share, u8), 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 keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
let mut manager = SmartcardManager::new(prompt.clone());
let mut encrypted_messages = encrypted_data.iter();
@ -527,24 +547,44 @@ impl<P: PromptHandler> Format for OpenPGP<P> {
panic!("unable to decrypt shard");
}
}
impl<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &Path {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
OpenPGP::<P>::discover_certs(self)
}
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();
fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
OpenPGP::<P>::discover_certs(self)
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<P: PromptHandler> KeyDiscovery<OpenPGP<P>> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP<P> as Format>::PublicKey>> {
impl KeyDiscovery<OpenPGP> for &Path {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
OpenPGP::discover_certs(self)
}
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
OpenPGP::discover_certs(self)
}
}
impl KeyDiscovery<OpenPGP> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
Ok(self.to_vec())
}
fn discover_private_keys(&self) -> Result<<OpenPGP<P> as Format>::PrivateKeyData> {
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
Ok(self.to_vec())
}
}
@ -557,7 +597,8 @@ fn get_encryption_keys<'a>(
openpgp::packet::key::UnspecifiedRole,
> {
cert.keys()
.alive()
// NOTE: this causes complications on Airgap systems
// .alive()
.revoked(false)
.supported()
.for_storage_encryption()
@ -605,12 +646,12 @@ fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec<Cert>)> {
// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a
// single message.
fn decrypt_with_manager<P: PromptHandler>(
fn decrypt_with_manager(
threshold: u8,
messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert],
policy: &dyn Policy,
manager: &mut SmartcardManager<P>,
manager: &mut SmartcardManager,
) -> Result<HashMap<KeyID, Vec<u8>>> {
let mut decrypted_messages = HashMap::new();
@ -655,11 +696,11 @@ fn decrypt_with_manager<P: PromptHandler>(
// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to
// decrypt messages with.
fn decrypt_with_keyring<P: PromptHandler>(
fn decrypt_with_keyring(
messages: &mut HashMap<KeyID, EncryptedMessage>,
certs: &[Cert],
policy: &NullPolicy,
keyring: &mut Keyring<P>,
keyring: &mut Keyring,
) -> Result<HashMap<KeyID, Vec<u8>>, Error> {
let mut decrypted_messages = HashMap::new();
@ -689,11 +730,11 @@ fn decrypt_with_keyring<P: PromptHandler>(
Ok(decrypted_messages)
}
fn decrypt_metadata<P: PromptHandler>(
fn decrypt_metadata(
message: &EncryptedMessage,
policy: &NullPolicy,
keyring: &mut Keyring<P>,
manager: &mut SmartcardManager<P>,
keyring: &mut Keyring,
manager: &mut SmartcardManager,
) -> Result<Vec<u8>> {
Ok(if keyring.is_empty() {
manager.load_any_card()?;

View File

@ -1,6 +1,4 @@
#![allow(clippy::expect_fun_call)]
use std::sync::{Arc, Mutex};
use std::{rc::Rc, sync::Mutex};
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{Error as PromptError, PromptHandler};
@ -25,21 +23,19 @@ pub enum Error {
Prompt(#[from] PromptError),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Keyring<P: PromptHandler> {
pub struct Keyring {
full_certs: Vec<Cert>,
root: Option<Cert>,
pm: Arc<Mutex<P>>,
pm: Rc<Mutex<Box<dyn PromptHandler>>>,
}
impl<P: PromptHandler> Keyring<P> {
pub fn new(certs: impl AsRef<[Cert]>, p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self {
impl Keyring {
pub fn new(certs: impl AsRef<[Cert]>, p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Self {
Self {
full_certs: certs.as_ref().to_vec(),
root: Default::default(),
root: Option::default(),
pm: p,
})
}
}
pub fn is_empty(&self) -> bool {
@ -62,7 +58,7 @@ impl<P: PromptHandler> Keyring<P> {
}
}
impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
impl VerificationHelper for &mut Keyring {
fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
Ok(ids
.iter()
@ -84,13 +80,23 @@ impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
aead_algo,
} => {}
MessageLayer::SignatureGroup { results } => {
for result in results {
if let Err(e) = result {
match &results[..] {
[Ok(_)] => {
return Ok(());
}
_ => {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
}
}
/*
for result in results {
if let Err(e) = result {
return Err(anyhow::anyhow!("Invalid signature: {e}"));
}
}
*/
}
}
}
@ -98,7 +104,7 @@ impl<P: PromptHandler> VerificationHelper for &mut Keyring<P> {
}
}
impl<P: PromptHandler> DecryptionHelper for &mut Keyring<P> {
impl DecryptionHelper for &mut Keyring {
fn decrypt<D>(
&mut self,
pkesks: &[PKESK],

View File

@ -1,12 +1,12 @@
#![allow(clippy::expect_fun_call)]
use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
rc::Rc,
sync::Mutex,
};
use keyfork_bug::{bug, POISONED_MUTEX};
use keyfork_prompt::{
prompt_validated_passphrase,
validators::{PinValidator, Validator},
Error as PromptError, Message, PromptHandler,
};
@ -71,21 +71,21 @@ fn format_name(input: impl AsRef<str>) -> String {
}
#[allow(clippy::module_name_repetitions)]
pub struct SmartcardManager<P: PromptHandler> {
pub struct SmartcardManager {
current_card: Option<Card<Open>>,
root: Option<Cert>,
pm: Arc<Mutex<P>>,
pm: Rc<Mutex<Box<dyn PromptHandler>>>,
pin_cache: HashMap<Fingerprint, String>,
}
impl<P: PromptHandler> SmartcardManager<P> {
pub fn new(p: Arc<Mutex<P>>) -> Result<Self> {
Ok(Self {
impl SmartcardManager {
pub fn new(p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Self {
Self {
current_card: None,
root: None,
pm: p,
pin_cache: Default::default(),
})
pin_cache: HashMap::default(),
}
}
// Sets the root cert, returning the old cert
@ -173,12 +173,11 @@ impl<P: PromptHandler> SmartcardManager<P> {
}
}
impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
impl VerificationHelper for &mut SmartcardManager {
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
#[allow(clippy::flat_map_option)]
Ok(ids
.iter()
.flat_map(|kh| self.root.as_ref().filter(|cert| cert.key_handle() == *kh))
.filter_map(|kh| self.root.as_ref().filter(|cert| cert.key_handle() == *kh))
.cloned()
.collect())
}
@ -193,12 +192,23 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
aead_algo,
} => {}
MessageLayer::SignatureGroup { results } => {
for result in results {
if let Err(e) = result {
// FIXME: anyhow leak
return Err(anyhow::anyhow!("Verification error: {}", e.to_string()));
match &results[..] {
[Ok(_)] => {
return Ok(());
}
_ => {
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Error validating signature; either multiple signatures were passed or the single signature was not valid"));
}
}
/*
for result in results {
if let Err(e) = result {
return Err(anyhow::anyhow!("Invalid signature: {e}"));
}
}
*/
}
}
}
@ -206,7 +216,7 @@ impl<P: PromptHandler> VerificationHelper for &mut SmartcardManager<P> {
}
}
impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
impl DecryptionHelper for &mut SmartcardManager {
fn decrypt<D>(
&mut self,
pkesks: &[PKESK],
@ -264,15 +274,11 @@ impl<P: PromptHandler> DecryptionHelper for &mut SmartcardManager<P> {
} else {
format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ")
};
let temp_pin =
self.pm
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_passphrase(&message, 3, &pin_validator)?;
let mut prompt = self.pm.lock().expect(bug!(POISONED_MUTEX));
let temp_pin = prompt_validated_passphrase(&mut **prompt, &message, 3, &pin_validator)?;
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
match verification_status {
#[allow(clippy::ignored_unit_patterns)]
Ok(_) => {
Ok(()) => {
self.pin_cache.insert(fp.clone(), temp_pin.clone());
pin.replace(temp_pin);
}

View File

@ -1,14 +1,17 @@
[package]
name = "keyfork"
version = "0.1.0"
version = "0.3.3"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
[features]
default = [
"completion",
"qrcode-decode-backend-rqrr",
"sequoia-crypto-backend-nettle",
"sequoia-crypto-backend-nettle",
]
completion = ["dep:clap_complete"]
@ -23,24 +26,30 @@ sequoia-crypto-backend-openssl = ["sequoia-openpgp/crypto-openssl"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
keyfork-bin = { version = "0.1.0", path = "../util/keyfork-bin", registry = "distrust" }
keyforkd = { version = "0.1.0", path = "../daemon/keyforkd", features = ["tracing"], registry = "distrust" }
keyforkd-client = { version = "0.1.0", path = "../daemon/keyforkd-client", default-features = false, features = ["ed25519"], registry = "distrust" }
keyfork-derive-openpgp = { version = "0.1.0", path = "../derive/keyfork-derive-openpgp", registry = "distrust" }
keyfork-derive-util = { version = "0.1.0", path = "../derive/keyfork-derive-util", default-features = false, features = ["ed25519"], registry = "distrust" }
keyfork-entropy = { version = "0.1.0", path = "../util/keyfork-entropy", registry = "distrust" }
keyfork-mnemonic-util = { version = "0.2.0", path = "../util/keyfork-mnemonic-util", registry = "distrust" }
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", registry = "distrust" }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", default-features = false, registry = "distrust" }
keyfork-shard = { version = "0.1.0", path = "../keyfork-shard", default-features = false, features = ["openpgp", "openpgp-card", "qrcode"], registry = "distrust" }
smex = { version = "0.1.0", path = "../util/smex", registry = "distrust" }
keyfork-bin = { workspace = true }
keyforkd = { workspace = true, features = ["tracing"] }
keyforkd-client = { workspace = true, default-features = false, features = ["ed25519"] }
keyfork-derive-util = { workspace = true, default-features = true }
keyfork-derive-openpgp = { workspace = true }
keyfork-derive-path-data = { workspace = true }
keyfork-entropy = { workspace = true }
keyfork-mnemonic = { workspace = true }
keyfork-prompt = { workspace = true }
keyfork-qrcode = { workspace = true, default-features = false }
keyfork-shard = { workspace = true, default-features = false, features = ["openpgp", "openpgp-card", "qrcode"] }
smex = { workspace = true }
clap = { version = "4.4.2", features = ["derive", "env", "wrap_help"] }
thiserror = "1.0.48"
serde = { version = "1.0.192", features = ["derive"] }
tokio = { version = "1.35.1", default-features = false, features = ["rt-multi-thread"] }
card-backend-pcsc = "0.5.0"
openpgp-card-sequoia = { version = "0.2.0", default-features = false }
openpgp-card = "0.4.1"
thiserror = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
card-backend-pcsc = { workspace = true }
openpgp-card-sequoia = { workspace = true }
openpgp-card = { workspace = true }
clap_complete = { version = "4.4.6", optional = true }
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] }
sequoia-openpgp = { workspace = true }
keyforkd-models.workspace = true
base64.workspace = true
nix = { version = "0.30.0", default-features = false, features = ["process"] }
shlex = "1.3.0"
tempfile.workspace = true

View File

@ -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(Self::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,
})
}
}

View File

@ -1,67 +1,292 @@
use super::Keyfork;
use clap::{Parser, Subcommand};
use super::{create, Keyfork};
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::{fmt::Display, io::Write, path::PathBuf};
use keyfork_derive_openpgp::{
openpgp::{
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
},
XPrvKey,
use keyfork_derive_openpgp::openpgp::{
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
Cert,
};
use keyfork_derive_path_data::paths;
use keyfork_derive_util::{
request::DerivationAlgorithm, DerivationIndex, DerivationPath, ExtendedPrivateKey as XPrv,
IndexError, PrivateKey,
};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyforkd_client::Client;
type OptWrite = Option<Box<dyn Write>>;
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)]
pub enum DeriveSubcommands {
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
/// ASCII Armor, a format usable by most programs using OpenPGP.
///
/// The key is generated with a 24-hour expiration time. The operation to set the expiration
/// time to a higher value is left to the user to ensure the key is usable by the user.
/// Certificates are created with a default expiration of one day, but may be configured to
/// expire later using the `KEYFORK_OPENPGP_EXPIRE` environment variable using values such as
/// "15d" (15 days), "1m" (one month), or "2y" (two years).
///
/// It is recommended to use the default expiration of one day and to change the expiration
/// using an external utility, to ensure the Certify key is usable.
#[command(name = "openpgp")]
OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format.
user_id: String,
},
OpenPGP(OpenPGP),
/// Derive an Ed25519 key for a specific algorithm, in a given format.
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)]
pub struct OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format.
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.
#[derive(ValueEnum, Clone, Debug)]
pub enum KeyFormat {
Hex,
Base64,
}
/// An invalid slug was provided.
#[derive(thiserror::Error, Debug)]
pub enum InvalidSlug {
/// The value provided was longer than four bytes.
#[error("The value provided was longer than four bytes: {0}")]
InvalidSize(usize),
/// The value provided was higher than the maximum derivation index.
#[error("The value provided was higher than the maximum derivation index: {0}")]
InvalidValue(#[from] IndexError),
}
#[derive(Clone, Debug)]
pub struct Slug(DerivationIndex);
impl std::str::FromStr for Slug {
type Err = InvalidSlug;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let bytes = s.as_bytes();
let mut parseable_bytes = [0u8; 4];
if bytes.len() <= 4 && !bytes.is_empty() {
parseable_bytes[(4 - bytes.len())..4].copy_from_slice(bytes);
} else {
return Err(InvalidSlug::InvalidSize(bytes.len()));
}
let slug = u32::from_be_bytes(parseable_bytes);
let index = DerivationIndex::new(slug, true)?;
Ok(Slug(index))
}
}
impl Display for Slug {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[allow(clippy::redundant_at_rest_pattern)]
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)]
pub struct Key {
/// The derivation algorithm to derive a key for.
derivation_algorithm: DerivationAlgorithm,
/// The output format.
#[arg(value_enum)]
format: KeyFormat,
/// A maximum of four bytes, used for creating the derivation path.
#[arg(value_parser = clap::value_parser!(Slug))]
slug: Slug,
}
impl DeriveSubcommands {
fn handle(&self, account: DerivationIndex) -> Result<()> {
fn handle(&self, account: DerivationIndex, is_public: bool, writer: OptWrite) -> Result<()> {
match self {
DeriveSubcommands::OpenPGP { user_id } => {
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let path = DerivationPath::default()
.chain_push(chain)
.chain_push(account);
// TODO: should this be customizable?
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
KeyFlags::empty()
.set_transport_encryption()
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(user_id.as_str());
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
for packet in cert.into_packets() {
packet.serialize(&mut w)?;
DeriveSubcommands::OpenPGP(opgp) => {
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)
}
w.finalize()?;
}
}
}
}
impl OpenPGP {
fn cert_from_xprv(&self, xprv: &keyfork_derive_openpgp::XPrv) -> 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 userid = UserID::from(&*self.user_id);
keyfork_derive_openpgp::derive(xprv, &subkeys, &userid).map_err(Into::into)
}
}
impl Deriver for OpenPGP {
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 = if let Some(writer) = writer { writer } else {
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(())
}
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()> {
let cert = self.cert_from_xprv(xprv)?;
let writer = if let Some(writer) = writer { writer } else {
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(())
}
}
impl Deriver for Key {
// HACK: We're abusing that we use the same key as OpenPGP. Maybe we should use ed25519_dalek.
type Prv = keyfork_derive_openpgp::XPrvKey;
const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519;
fn derivation_path(&self) -> DerivationPath {
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 => {
use base64::prelude::*;
(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)?;
}
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(())
}
}
@ -69,7 +294,7 @@ impl DeriveSubcommands {
#[derive(Parser, Debug, Clone)]
pub struct Derive {
#[command(subcommand)]
command: DeriveSubcommands,
pub(crate) command: DeriveSubcommands,
/// Account ID. Required for all derivations.
///
@ -77,12 +302,45 @@ pub struct Derive {
/// account ID can often come as a hindrance in the future. As such, it is always required. If
/// the account ID is not relevant, it is assumed to be `0`.
#[arg(long, global = true, default_value = "0")]
account_id: u32,
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 {
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
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)),
)
}
}

View File

@ -1,6 +1,42 @@
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 std::fmt::Display;
use std::{
collections::{HashMap, HashSet},
fmt::Display,
fs::File,
io::{IsTerminal, Write},
path::{Path, PathBuf},
str::FromStr,
};
use keyfork_derive_openpgp::{
openpgp::{
self,
armor::{Kind, Writer},
packet::{signature::SignatureBuilder, UserID},
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)]
pub enum SeedSize {
@ -59,6 +95,7 @@ impl From<&SeedSize> for usize {
}
}
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum MnemonicSeedSourceParseError {
#[error("Expected one of system, playing, tarot, dice")]
@ -96,24 +133,41 @@ impl std::str::FromStr for 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 {
SeedSize::Bits128 => 128,
SeedSize::Bits256 => 256,
};
let seed = match self {
MnemonicSeedSource::System => {
keyfork_entropy::generate_entropy_of_size(size / 8)?
}
MnemonicSeedSource::System => keyfork_entropy::generate_entropy_of_size(size / 8)?,
MnemonicSeedSource::Playing => todo!(),
MnemonicSeedSource::Tarot => todo!(),
MnemonicSeedSource::Dice => todo!(),
};
let mnemonic = keyfork_mnemonic_util::Mnemonic::from_bytes(&seed)?;
Ok(mnemonic.to_string())
let mnemonic = keyfork_mnemonic::Mnemonic::try_from_slice(&seed)?;
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(path: &Path) -> impl Fn(std::io::Error) -> Error + use<'_> {
|e| Error::IOContext(e, path.to_path_buf())
}
#[derive(Subcommand, Clone, Debug)]
pub enum MnemonicSubcommands {
/// Generate a mnemonic using a given entropy source.
@ -124,6 +178,10 @@ pub enum MnemonicSubcommands {
/// 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
/// (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 {
/// The source from where a seed is created.
#[arg(long, value_enum, default_value_t = Default::default())]
@ -132,17 +190,758 @@ pub enum MnemonicSubcommands {
/// The size of the mnemonic, in bits.
#[arg(long, default_value_t = Default::default())]
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 {
if let Some(p) = optional_path {
p.as_ref().to_path_buf()
} else {
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" | "gpg") => false,
Some("asc") => true,
_ => {
eprintln!("unable to determine whether to armor file: {path}", path = path.display());
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.into());
}
let (threshold, max) = if let Some(t) = threshold.zip(max) { t } else {
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")
.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 #{}",
(u16::from(i)) + 1,
(u16::from(index)) + 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,
u8::try_from(certs.len()).expect("provided more than u8::MAX certs"),
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_key = root_xprv.derive_path(&opgp.derivation_path().chain_push(account))?;
if *public {
opgp.derive_public_with_xprv(writer, &derived_key)?;
} else {
opgp.derive_with_xprv(writer, &derived_key)?;
}
}
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_key = root_xprv.derive_path(&key.derivation_path().chain_push(account))?;
if *public {
key.derive_public_with_xprv(writer, &derived_key)?;
} else {
key.derive_with_xprv(writer, &derived_key)?;
}
}
}
Ok(())
}
impl MnemonicSubcommands {
#[allow(clippy::too_many_lines)]
pub fn handle(
&self,
_m: &Mnemonic,
_keyfork: &Keyfork,
) -> Result<String, Box<dyn std::error::Error>> {
) -> Result<(), Box<dyn std::error::Error>> {
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(Vec::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(Vec::is_empty);
will_print_mnemonic = will_print_mnemonic && shard.is_none()
|| shard.as_ref().is_some_and(Vec::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: HashSet<u32> = HashSet::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(())
}
}
}
}

View File

@ -5,7 +5,11 @@ mod mnemonic;
mod provision;
mod recover;
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.
#[derive(Parser, Clone, Debug)]
@ -16,6 +20,7 @@ pub struct Keyfork {
pub command: KeyforkCommands,
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand, Clone, Debug)]
pub enum KeyforkCommands {
/// Derive keys of various formats. These commands require that the Keyfork server is running,
@ -57,9 +62,6 @@ pub enum KeyforkCommands {
/// leaked by any individual deriver.
Recover(recover::Recover),
/// Utilities to automatically manage the setup of Keyfork.
Wizard(wizard::Wizard),
/// Print an autocompletion file to standard output.
///
/// Keyfork does not manage the installation of completion files. Consult the documentation for
@ -79,8 +81,7 @@ impl KeyforkCommands {
d.handle(keyfork)?;
}
KeyforkCommands::Mnemonic(m) => {
let response = m.command.handle(m, keyfork)?;
println!("{response}");
m.command.handle(m, keyfork)?;
}
KeyforkCommands::Shard(s) => {
s.command.handle(s, keyfork)?;
@ -91,19 +92,11 @@ impl KeyforkCommands {
KeyforkCommands::Recover(r) => {
r.handle(keyfork)?;
}
KeyforkCommands::Wizard(w) => {
w.handle(keyfork)?;
}
#[cfg(feature = "completion")]
KeyforkCommands::Completion { shell } => {
let mut command = Keyfork::command();
let command_name = command.get_name().to_string();
clap_complete::generate(
*shell,
&mut command,
command_name,
&mut std::io::stdout(),
);
clap_complete::generate(*shell, &mut command, command_name, &mut std::io::stdout());
}
}
Ok(())

View File

@ -3,81 +3,138 @@ use crate::config;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_derive_util::{DerivationIndex, ExtendedPrivateKey};
mod openpgp;
type Identifier = (String, Option<String>);
#[derive(Debug, Clone)]
pub enum Provisioner {
OpenPGPCard(OpenPGPCard),
OpenPGPCard(openpgp::OpenPGPCard),
Shard(openpgp::Shard),
}
impl std::fmt::Display for Provisioner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Provisioner::OpenPGPCard(_) => f.write_str("openpgp-card"),
}
f.write_str(self.identifier())
}
}
impl Provisioner {
fn discover(&self) -> Vec<(String, Option<String>)> {
pub fn identifier(&self) -> &'static str {
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,
provisioner: config::Provisioner,
provisioner: &config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
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 {
fn value_variants<'a>() -> &'a [Self] {
&[Self::OpenPGPCard(OpenPGPCard)]
&[
Self::OpenPGPCard(openpgp::OpenPGPCard),
Self::Shard(openpgp::Shard),
]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(PossibleValue::new(match self {
Self::OpenPGPCard(_) => "openpgp-card",
}))
Some(PossibleValue::new(self.identifier()))
}
}
#[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 {
type PrivateKey: keyfork_derive_util::PrivateKey + Clone;
/// Discover all known places the formatted key can be deployed to.
fn discover(&self) -> Vec<(String, Option<String>)> {
vec![]
fn discover(&self) -> Result<Vec<Identifier>, Box<dyn std::error::Error>> {
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.
fn provision(&self, p: config::Provisioner) -> Result<(), Box<dyn std::error::Error>>;
}
#[derive(Clone, Debug)]
pub struct OpenPGPCard;
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!()
}
fn provision(
&self,
xprv: &keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
p: &config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>>;
}
#[derive(Subcommand, Clone, Debug)]
@ -94,15 +151,27 @@ pub struct Provision {
#[command(subcommand)]
pub subcommand: Option<ProvisionSubcommands>,
provisioner_name: Provisioner,
pub provisioner_name: Provisioner,
/// Account ID.
#[arg(long, required(true))]
account_id: Option<u32>,
#[arg(long, default_value = "0")]
pub account_id: u32,
/// Identifier of the hardware to deploy to, listable by running the `discover` subcommand.
#[arg(long, required(true))]
identifier: Option<String>,
#[arg(long)]
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
@ -118,10 +187,9 @@ impl TryFrom<Provision> for config::Provisioner {
fn try_from(value: Provision) -> Result<Self, Self::Error> {
Ok(Self {
name: value.provisioner_name.to_string(),
account: value.account_id.ok_or(MissingField("account_id"))?,
account: value.account_id,
identifier: value.identifier.ok_or(MissingField("identifier"))?,
metadata: Default::default(),
metadata: Option::default(),
})
}
}
@ -130,7 +198,7 @@ impl Provision {
pub fn handle(&self, _keyfork: &Keyfork) -> Result<(), Box<dyn std::error::Error>> {
match self.subcommand {
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() {
println!("Identifier: {identifier}");
if let Some(context) = context {
@ -142,7 +210,20 @@ impl Provision {
}
}
None => {
self.provisioner_name.provision(self.clone().try_into()?)?;
let provisioner_with_identifier = if self.identifier.is_some() {
self.clone()
} else {
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(())

View File

@ -0,0 +1,156 @@
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;
pub type CardList = Vec<(String, Option<String>)>;
fn discover_cards() -> Result<CardList, 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, &subkeys, &userid)?;
if !provisioner
.metadata
.as_ref()
.is_some_and(|m| m.contains_key("_skip_cert_output"))
{
let cert_output = if let Some(cert_output) =
provisioner.metadata.as_ref().and_then(|m| m.get("output"))
{
PathBuf::from(cert_output)
} else {
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.into());
}
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)
}
}

View File

@ -1,9 +1,19 @@
use super::Keyfork;
use clap::{Parser, Subcommand};
use nix::{
sys::wait::waitpid,
unistd::{fork, ForkResult},
};
use std::path::PathBuf;
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{
default_handler, prompt_validated_wordlist,
validators::{
mnemonic::{MnemonicChoiceValidator, WordLength},
Validator,
},
};
use keyfork_shard::{remote_decrypt, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -35,8 +45,8 @@ impl RecoverSubcommands {
} => {
let content = std::fs::read_to_string(shard_file)?;
if content.contains("BEGIN PGP MESSAGE") {
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt_handler = default_terminal()?;
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt_handler = default_handler()?;
// TODO: remove .clone() by making handle() consume self
let seed = openpgp.decrypt_all_shards_to_secret(
key_discovery.as_deref(),
@ -54,21 +64,15 @@ impl RecoverSubcommands {
Ok(seed)
}
RecoverSubcommands::Mnemonic {} => {
use keyfork_prompt::{
validators::{
mnemonic::{MnemonicChoiceValidator, WordLength},
Validator,
},
PromptHandler,
};
let mut term = default_terminal()?;
let mut prompt_handler = default_handler()?;
let validator = MnemonicChoiceValidator {
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
};
let mnemonic = term.prompt_validated_wordlist::<English, _>(
let mnemonic = prompt_validated_wordlist::<English, _>(
&mut *prompt_handler,
"Mnemonic: ",
3,
validator.to_fn(),
&*validator.to_fn(),
)?;
Ok(mnemonic.to_bytes())
}
@ -80,12 +84,32 @@ impl RecoverSubcommands {
pub struct Recover {
#[command(subcommand)]
command: RecoverSubcommands,
/// Daemonize the server once started, restoring control back to the shell.
#[arg(long, global = true)]
daemon: bool,
}
impl Recover {
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
let seed = self.command.handle()?;
let mnemonic = Mnemonic::from_bytes(&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()
.enable_all()
.build()

View File

@ -1,6 +1,6 @@
use super::Keyfork;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_prompt::{default_terminal, DefaultTerminal};
use keyfork_prompt::default_handler;
use keyfork_shard::Format as _;
use std::{
io::{stdin, stdout, Read, Write},
@ -50,6 +50,14 @@ trait ShardExec {
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
) -> 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)]
@ -64,7 +72,7 @@ impl ShardExec for OpenPGP {
secret: &[u8],
output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>> {
let opgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let opgp = keyfork_shard::openpgp::OpenPGP;
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
}
@ -74,8 +82,8 @@ impl ShardExec for OpenPGP {
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> {
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt = default_terminal()?;
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt = default_handler()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input, prompt)?;
write!(output, "{}", smex::encode(bytes))?;
@ -87,11 +95,37 @@ impl ShardExec for OpenPGP {
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let openpgp = keyfork_shard::openpgp::OpenPGP::<DefaultTerminal>::new();
let prompt = default_terminal()?;
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt = default_handler()?;
openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?;
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::{
armor::{Kind, Writer},
serialize::Marshal,
};
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)]
@ -141,6 +175,20 @@ pub enum ShardSubcommands {
/// The path to discover private keys from.
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 {
@ -209,6 +257,31 @@ impl ShardSubcommands {
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}"),
}
}
}
}
}

View File

@ -1,229 +0,0 @@
use super::Keyfork;
use clap::{Parser, Subcommand};
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
use keyfork_derive_openpgp::{
openpgp::{self, packet::UserID, types::KeyFlags, Cert},
XPrv,
};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_prompt::{
validators::{PinValidator, Validator},
Message, PromptHandler, DefaultTerminal, default_terminal
};
use keyfork_shard::{Format, openpgp::OpenPGP};
#[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>;
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 mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let mut shrd_u32 = [0u8; 4];
shrd_u32[..].copy_from_slice(&"shrd".bytes().collect::<Vec<u8>>());
let account = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let subkey = DerivationIndex::new(u32::from(index), true)?;
let path = DerivationPath::default()
.chain_push(chain)
.chain_push(account)
.chain_push(subkey);
let xprv = XPrv::new(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(())
}
fn generate_shard_secret(
threshold: u8,
max: u8,
keys_per_shard: u8,
output_file: &Option<PathBuf>,
) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_const_size::<{256 / 8}>()?;
let mut pm = default_terminal()?;
let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new();
let stdout = std::io::stdout();
if output_file.is_none() {
assert!(
!stdout.is_terminal(),
"not printing shard to terminal, redirect output"
);
}
let user_pin_validator = PinValidator {
min_length: Some(6),
..Default::default()
}
.to_fn();
let admin_pin_validator = PinValidator {
min_length: Some(8),
..Default::default()
}
.to_fn();
for index in 0..max {
let cert = derive_key(seed, index)?;
for i in 0..keys_per_shard {
pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}",
i + 1,
index + 1,
)))?;
let card_backend = loop {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
break c;
}
pm.prompt_message(Message::Text(
"No smart card was found. Please plug in a smart card and press enter"
.to_string(),
))?;
};
let user_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard User PIN: ",
3,
&user_pin_validator,
)?;
let admin_pin = pm.prompt_validated_passphrase(
"Please enter the new smartcard Admin PIN: ",
3,
&admin_pin_validator,
)?;
factory_reset_current_card(
&mut seen_cards,
user_pin.trim(),
admin_pin.trim(),
&cert,
card_backend,
)?;
}
certs.push(cert);
}
let opgp = OpenPGP::<DefaultTerminal>::new();
if let Some(output_file) = output_file {
let output = File::create(output_file)?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else {
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], std::io::stdout())?;
}
Ok(())
}
#[derive(Subcommand, Clone, Debug)]
pub enum WizardSubcommands {
/// Create a 256 bit secret and shard the secret to smart cards.
///
/// Smart cards will need to be plugged in periodically during the wizard, where they will be factory reset and
/// provisioned to `m/pgp'/shrd'/<share index>`. The secret can then be recovered with `keyfork recover shard` or
/// `keyfork recover remote-shard`. The share file will be printed to standard output.
GenerateShardSecret {
/// The minimum amount of keys required to decrypt the secret.
#[arg(long)]
threshold: u8,
/// 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>,
},
}
impl WizardSubcommands {
fn handle(&self) -> Result<()> {
match self {
WizardSubcommands::GenerateShardSecret {
threshold,
max,
keys_per_shard,
output,
} => generate_shard_secret(*threshold, *max, *keys_per_shard, output),
}
}
}
#[derive(Parser, Debug, Clone)]
pub struct Wizard {
#[command(subcommand)]
command: WizardSubcommands,
}
impl Wizard {
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
self.command.handle()?;
Ok(())
}
}

View File

@ -2,20 +2,19 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Mnemonic {
pub hash: String,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Provisioner {
pub name: String,
pub account: u32,
pub identifier: String,
pub metadata: Option<HashMap<String, String>>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config {
pub mnemonic: Mnemonic,
pub provisioner: Vec<Provisioner>,

View File

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")]
#![allow(clippy::module_name_repetitions)]
use std::process::ExitCode;
@ -8,8 +7,10 @@ use clap::Parser;
use keyfork_bin::{Bin, ClosureBin};
pub mod clap_ext;
mod cli;
mod config;
mod openpgp_card;
fn main() -> ExitCode {
let bin = ClosureBin::new(|| {

View File

@ -0,0 +1,113 @@
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 {
break user_pin;
}
pm.prompt_message(Message::Text("User PINs did not match. Retrying.".into()))?;
};
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 {
break admin_pin;
}
pm.prompt_message(Message::Text("Admin PINs did not match. Retrying.".into()))?;
};
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)
}

View File

@ -1,10 +1,13 @@
[package]
name = "keyfork-qrcode"
version = "0.1.0"
version = "0.1.3"
repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
@ -14,9 +17,10 @@ decode-backend-rqrr = ["dep:rqrr"]
decode-backend-zbar = ["dep:keyfork-zbar"]
[dependencies]
keyfork-bug = { version = "0.1.0", path = "../../util/keyfork-bug", registry = "distrust" }
keyfork-zbar = { version = "0.1.0", path = "../keyfork-zbar", optional = true, registry = "distrust" }
image = { version = "0.24.7", default-features = false, features = ["jpeg"] }
rqrr = { version = "0.6.0", optional = true }
thiserror = "1.0.56"
v4l = "0.14.0"
keyfork-bug = { workspace = true }
keyfork-zbar = { workspace = true, optional = true, features = ["image"] }
image = { workspace = true, default-features = false, features = ["jpeg"] }
rqrr = { version = "0.9.0", optional = true }
thiserror = { workspace = true }
v4l = { workspace = true }
cfg-if = "1.0.0"

View File

@ -1,11 +1,11 @@
//!
#![allow(missing_docs)]
use std::time::Duration;
use keyfork_qrcode::scan_camera;
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 {
println!("{scanned_text}");
}

View File

@ -2,20 +2,23 @@
use keyfork_bug as bug;
use image::io::Reader as ImageReader;
use bug::POISONED_MUTEX;
use image::{ImageBuffer, ImageReader, Luma};
use std::{
io::{Cursor, Write},
time::{Duration, SystemTime},
process::{Command, Stdio},
sync::{mpsc::channel, Arc, Condvar, Mutex},
time::{Duration, Instant},
};
use v4l::{
buffer::Type,
io::{userptr::Stream, traits::CaptureStream},
io::{traits::CaptureStream, userptr::Stream},
video::Capture,
FourCC,
Device,
Device, FourCC,
};
type Image = ImageBuffer<Luma<u8>, Vec<u8>>;
/// A QR code could not be generated.
#[derive(thiserror::Error, Debug)]
pub enum QRGenerationError {
@ -34,10 +37,10 @@ pub enum QRCodeScanError {
/// The camera could not load the requested format.
#[error("Camera could not use {expected} format, instead used {actual}")]
CameraGaveBadFormat {
/// The expected format, in FourCC format.
/// The expected format, in `FourCC` format.
expected: String,
/// The actual format, in FourCC format.
/// The actual format, in `FourCC` format.
actual: String,
},
@ -102,62 +105,188 @@ pub fn qrencode(
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
/// Continuously scan the `index`-th camera for a QR code.
#[cfg(feature = "decode-backend-rqrr")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
fmt.fourcc = FourCC::new(b"MPG1");
device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = SystemTime::now();
trait Scanner {
fn scan_image(&mut self, image: Image) -> Option<String>;
}
while SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::from_secs(0))
< timeout
{
let (buffer, _) = stream.next()?;
let image = ImageReader::new(Cursor::new(buffer))
.with_guessed_format()?
.decode()?
.to_luma8();
let mut image = rqrr::PreparedImage::prepare(image);
for grid in image.detect_grids() {
if let Ok((_, content)) = grid.decode() {
return Ok(Some(content))
#[cfg(feature = "decode-backend-zbar")]
mod zbar {
use super::{Image, 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(),
}
}
}
Ok(None)
impl Scanner for Zbar {
fn scan_image(&mut self, image: Image) -> 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::{Image, Scanner};
pub struct Rqrr;
impl Scanner for Rqrr {
fn scan_image(&mut self, image: Image) -> 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, clippy::cast_precision_loss)]
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}");
std::thread::sleep(std::time::Duration::from_secs(5));
}
#[derive(Debug)]
struct ScanQueue {
shutdown: bool,
images: Vec<Image>,
}
/// Continuously scan the `index`-th camera for a QR code.
#[cfg(feature = "decode-backend-zbar")]
///
/// # Errors
///
/// The function may return an error if the hardware is unable to scan video or if an image could
/// not be decoded.
///
/// # Panics
///
/// The function may panic if a mutex is poisoned by a thread panicking, which should
/// only happen during a mutex, or if it can't send a message over the mpsc channel.
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
let mut fmt = device
.format()
.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 = SystemTime::now();
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
let start = Instant::now();
while SystemTime::now()
.duration_since(start)
.unwrap_or(Duration::from_secs(0))
< 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()));
#[allow(unused)]
let mut count = 0;
let thread_count = 4;
std::thread::scope(|scope| {
let scan_queue = ScanQueue {
shutdown: false,
images: vec![],
};
let arced = Arc::new((Mutex::new(scan_queue), Condvar::new()));
let (tx, rx) = channel();
for _ in 0..thread_count {
let tx = tx.clone();
let arced = arced.clone();
scope.spawn(move || {
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")
}
};
let (queue_mutex, condvar) = &*arced;
loop {
// NOTE: Carrying the `queue` variable through the loop, so we can
// pass it through without re-locking, means that we don't drop the
// lock on the mutex. Therefore, we unlock, then immediately
// re-lock when we pass the value to wait_while().
//
// By holding onto the queue until we pass it back to the Condvar,
// and checking shutdown, we ensure that there's no way we miss the
// shutdown being set before we release the guard on the queue.
let queue = queue_mutex.lock().expect(bug::bug!(POISONED_MUTEX));
if queue.shutdown {
break;
}
let mut queue = condvar
.wait_while(queue, |queue| {
queue.images.is_empty() && !queue.shutdown
})
.expect(bug::bug!(POISONED_MUTEX));
if let Some(image) = queue.images.pop() {
// drop the queue here since this is what's going to take time
drop(queue);
if let Some(content) = scanner.scan_image(image) {
if tx.send(content).is_err() {
break;
}
}
}
}
});
}
}
Ok(None)
while Instant::now().duration_since(start) < timeout {
if let Ok(content) = rx.try_recv() {
arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true;
arced.1.notify_all();
return Ok(Some(content));
}
count += 1;
let (buffer, _) = stream.next()?;
let image = ImageReader::new(Cursor::new(buffer))
.with_guessed_format()?
.decode()?
.to_luma8();
arced
.0
.lock()
.expect(bug::bug!(POISONED_MUTEX))
.images
.push(image);
arced.1.notify_one();
}
// dbg_elapsed(count, start);
arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true;
arced.1.notify_all();
Ok(None)
})
}

View File

@ -5,10 +5,13 @@ repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[build-dependencies]
bindgen = { version = "0.68", default-features = false, features = ["runtime"] }
bindgen = { version = "0.70", default-features = false, features = ["runtime"] }
pkg-config = "0.3"

View File

@ -45,7 +45,7 @@ fn main() -> Result<()> {
if let Err(e) = generate_bindings_file() {
eprintln!("Building zbar-sys failed: {e}");
eprintln!("Ensure zbar headers, libclang, and pkg-config are installed");
return Err(e)
return Err(e);
}
Ok(())

View File

@ -1,4 +1,5 @@
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
#![allow(missing_docs)]
#![allow(clippy::unreadable_literal, clippy::pub_underscore_fields)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

View File

@ -1,20 +1,24 @@
[package]
name = "keyfork-zbar"
version = "0.1.0"
version = "0.1.2"
repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["image"]
bin = ["image"]
image = ["dep:image"]
[dependencies]
keyfork-zbar-sys = { version = "0.1.0", path = "../keyfork-zbar-sys", registry = "distrust" }
image = { version = "0.24.7", default-features = false, optional = true }
thiserror = "1.0.56"
keyfork-zbar-sys = { workspace = true }
image = { workspace = true, default-features = false, optional = true }
thiserror = { workspace = true }
[dev-dependencies]
v4l = "0.14.0"
v4l = { workspace = true }

View File

@ -7,7 +7,7 @@ use std::{
use keyfork_zbar::{image::Image, image_scanner::ImageScanner};
use image::io::Reader as ImageReader;
use image::ImageReader;
use v4l::{
buffer::Type,
io::{traits::CaptureStream, userptr::Stream},
@ -33,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.decode()?,
);
if let Some(symbol) = scanner.scan_image(&image).get(0) {
if let Some(symbol) = scanner.scan_image(&image).first() {
println!("{}", String::from_utf8_lossy(symbol.data()));
return Ok(());
}

View File

@ -20,16 +20,13 @@ impl Image {
/// Link: [`sys::zbar_image_set_format`]
///
/// A FourCC code can be given in the format:
/// A `FourCC` code can be given in the format:
///
/// ```rust,ignore
/// self.set_format(b"Y800")
/// ```
pub(crate) fn set_format(&mut self, fourcc: &[u8; 4]) {
let fourcc: u64 = fourcc[0] as u64
| ((fourcc[1] as u64) << 8)
| ((fourcc[2] as u64) << 16)
| ((fourcc[3] as u64) << 24);
let fourcc = std::os::raw::c_ulong::from(u32::from_le_bytes(*fourcc));
unsafe { sys::zbar_image_set_format(self.inner, fourcc) }
}
@ -43,12 +40,7 @@ impl Image {
/// Accepts raw data in the configured format. See: [`Image::set_format`]
fn set_data(&mut self, data: Vec<u8>) {
unsafe {
sys::zbar_image_set_data(
self.inner,
data.as_ptr().cast(),
data.len() as u64,
None,
)
sys::zbar_image_set_data(self.inner, data.as_ptr().cast(), data.len() as u64, None);
}
// keep data in self to avoid use after free when data goes out of scope
let _ = self.inner_data.insert(data);
@ -58,7 +50,7 @@ impl Image {
#[cfg(feature = "image")]
mod impls {
use super::*;
use image::{DynamicImage, GenericImageView};
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma};
impl From<DynamicImage> for Image {
fn from(value: DynamicImage) -> Self {
@ -70,6 +62,17 @@ mod impls {
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 {

View File

@ -22,7 +22,7 @@ pub struct ImageScanner {
}
impl ImageScanner {
/// create a new ImageScanner.
/// Create a new `ImageScanner`.
///
/// Link: [`sys::zbar_image_scanner_create`]
pub fn new() -> Self {
@ -31,7 +31,7 @@ impl ImageScanner {
}
}
/// Set a configuration option for the ImageScanner.
/// Set a configuration option.
///
/// Link: [`sys::zbar_image_scanner_set_config`]
///
@ -58,10 +58,7 @@ impl ImageScanner {
/// Link: [`sys::zbar_scan_image`]
///
/// TODO: return an iterator over scanned values
pub fn scan_image(
&mut self,
image: &Image,
) -> Vec<Symbol> {
pub fn scan_image(&mut self, image: &Image) -> Vec<Symbol> {
unsafe { sys::zbar_scan_image(self.inner, image.inner) };
let mut result = vec![];
let mut symbol = unsafe { sys::zbar_image_first_symbol(image.inner) };
@ -70,7 +67,7 @@ impl ImageScanner {
let symbol_data = unsafe { sys::zbar_symbol_get_data(symbol) };
let symbol_data_len = unsafe { sys::zbar_symbol_get_data_length(symbol) };
let symbol_slice = unsafe {
std::slice::from_raw_parts(symbol_data as *const u8, symbol_data_len as usize)
std::slice::from_raw_parts(symbol_data.cast::<u8>(), symbol_data_len as usize)
};
result.push(Symbol::new(symbol_type, symbol_slice));
symbol = unsafe { sys::zbar_symbol_next(symbol) };

View File

@ -11,6 +11,6 @@ pub use sys::zbar_config_e as Config;
pub use sys::zbar_modifier_e as Modifier;
pub use sys::zbar_orientation_e as Orientation;
pub mod image_scanner;
pub mod image;
pub mod image_scanner;
pub mod symbol;

View File

@ -1,4 +1,6 @@
//!
//! A Symbol represents some form of encoded data.
#![allow(clippy::used_underscore_binding)]
use super::sys;

14
crates/tests/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "keyfork-tests"
version = "0.1.0"
edition = "2021"
publish = false
license = "MIT"
[lints]
workspace = true
[dependencies]
assert_cmd = "2.0.16"
keyforkd = { workspace = true, features = ["default"] }
sequoia-openpgp = { workspace = true, features = ["crypto-nettle"] }

View File

@ -0,0 +1 @@
mod openpgp;

View File

@ -0,0 +1,56 @@
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");
}
assert!(key_formats.is_empty(), "remaining key formats: {key_formats:?}");
}

View File

@ -0,0 +1 @@
mod derive;

4
crates/tests/src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
#![allow(missing_docs)]
#[cfg(test)]
mod keyfork;

View File

@ -4,9 +4,12 @@ version = "0.1.0"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[dev-dependencies]
anyhow = "1.0.79"
anyhow = { workspace = true }

View File

@ -45,6 +45,7 @@ use std::process::ExitCode;
/// A result that may contain any error.
pub type ProcessResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
#[allow(clippy::needless_pass_by_value)]
fn report_err(e: Box<dyn std::error::Error>) {
eprintln!("Unable to run command: {e}");
let mut source = e.source();
@ -68,7 +69,7 @@ pub trait Bin {
#[allow(clippy::missing_errors_doc)]
fn validate_args(&self, args: impl Iterator<Item = String>) -> ProcessResult<Self::Args>;
/// Run the binary
/// Run the binary
#[allow(clippy::missing_errors_doc)]
fn run(&self, args: Self::Args) -> ProcessResult;
@ -102,10 +103,13 @@ pub trait Bin {
/// A Bin that doesn't take any arguments.
pub struct ClosureBin<F: Fn() -> ProcessResult> {
closure: F
closure: F,
}
impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
impl<F> ClosureBin<F>
where
F: Fn() -> ProcessResult,
{
/// Create a new Bin from a closure.
///
/// # Examples
@ -120,13 +124,14 @@ impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
/// bin.main();
/// ```
pub fn new(closure: F) -> Self {
Self {
closure
}
Self { closure }
}
}
impl<F> Bin for ClosureBin<F> where F: Fn() -> ProcessResult {
impl<F> Bin for ClosureBin<F>
where
F: Fn() -> ProcessResult,
{
type Args = ();
fn validate_args(&self, _args: impl Iterator<Item = String>) -> ProcessResult<Self::Args> {

View File

@ -1,9 +1,12 @@
[package]
name = "keyfork-bug"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -16,6 +16,12 @@
//! ```
//!
//! ```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 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
/// 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.

View File

@ -0,0 +1,13 @@
[package]
name = "keyfork-crossterm-ioctl-shim"
version = "0.1.0"
description = "A shim for working with crossterm ioctls with custom fd's"
repository = "https://git.distrust.co/public/keyfork"
edition = "2024"
license = "MIT"
[lints]
workspace = true
[dependencies]
libc = "0.2.172"

View File

@ -0,0 +1,113 @@
//! A shim for replacing some Crossterm methods with methods tied to a file descriptor.
use libc::termios as Termios;
use std::{os::fd::RawFd, io::IsTerminal};
/// The provided file descriptor was not a terminal.
#[derive(Debug)]
pub struct NotATerminal;
impl std::fmt::Display for NotATerminal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("The provided file descriptor is not a terminal")
}
}
impl std::error::Error for NotATerminal {}
/// A terminal controller.
pub struct TerminalIoctl {
fd: RawFd,
stored_termios: Option<Termios>,
}
type Result<T, E = std::io::Error> = std::result::Result<T, E>;
fn assert_io(result: i32) -> Result<i32> {
if result == -1 {
Err(std::io::Error::last_os_error())
} else {
Ok(result)
}
}
impl TerminalIoctl {
/// Construct a new controller for the given file descriptor.
///
/// # Errors
///
/// The method may return an error if the file descriptor is not a terminal.
pub fn new(fd: RawFd) -> Result<Self> {
// SAFETY: We do not invoke any function that closes the file descriptor, and
// the borrowed fd is dropped as the function returns.
let borrowed_fd = unsafe {
std::os::fd::BorrowedFd::borrow_raw(fd)
};
if !borrowed_fd.is_terminal() {
return Err(std::io::Error::other(NotATerminal));
}
Ok(Self {
fd,
stored_termios: None,
})
}
fn get_termios(&self) -> Result<Termios> {
let mut termios = unsafe { std::mem::zeroed() };
assert_io(unsafe { libc::tcgetattr(self.fd, &mut termios) })?;
Ok(termios)
}
/// Enable raw mode for the given terminal.
///
/// Replaces: [`crossterm::terminal::enable_raw_mode`].
///
/// # Errors
///
/// The method may return an error if
pub fn enable_raw_mode(&mut self) -> Result<()> {
if self.stored_termios.is_none() {
let mut termios = self.get_termios()?;
let original_mode_ios = termios;
unsafe { libc::cfmakeraw(&mut termios) };
assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?;
self.stored_termios = Some(original_mode_ios);
}
Ok(())
}
/// Disable raw mode for the given terminal.
///
/// Replaces: [`crossterm::terminal::disable_raw_mode`].
///
/// # Errors
///
/// The method may propagate errors encountered when interacting with the terminal.
pub fn disable_raw_mode(&mut self) -> Result<()> {
if let Some(termios) = self.stored_termios.take() {
assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?;
}
Ok(())
}
/// Return the size for the given terminal.
///
/// Replaces: [`crossterm::terminal::size`].
///
/// # Errors
///
/// The method may propagate errors encountered when interacting with the terminal.
pub fn size(&self) -> Result<(u16, u16)> {
let mut size = libc::winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
assert_io(unsafe { libc::ioctl(self.fd, libc::TIOCGWINSZ, &mut size) })?;
Ok((size.ws_col, size.ws_row))
}
}

View File

@ -1,42 +0,0 @@
# Build only pushed (merged) master or any pull request. This avoids the
# pull request to be build twice.
branches:
only:
- master
language: rust
rust:
- stable
- nightly
os:
- linux
- windows
- osx
git:
depth: 1
quiet: true
matrix:
allow_failures:
- rust: nightly
before_script:
- export PATH=$PATH:/home/travis/.cargo/bin
- rustup component add rustfmt
- rustup component add clippy
script:
- cargo fmt --version
- rustup --version
- rustc --version
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo fmt --all -- --check; fi
- cargo clippy -- -D clippy::all
- cargo build
- cargo test --lib -- --nocapture --test-threads 1
- cargo test --lib --features serde -- --nocapture --test-threads 1
- cargo test --lib --features event-stream -- --nocapture --test-threads 1
- cargo test --all-features -- --nocapture --test-threads 1
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo package; fi

View File

@ -1,744 +0,0 @@
# Version 0.27.1
## Added ⭐
- Add support for (de)serializing `Reset` `Color`
# Version 0.27
## Added ⭐
- Add `NO_COLOR` support (https://no-color.org/)
- Add option to force overwrite `NO_COLOR` (#802)
- Add support for scroll left/right events on windows and unix systems (#788).
- Add `window_size` function to fetch pixel width/height of screen for more sophisticated rendering in terminals.
- Add support for deserializing hex color strings to `Color` e.g #fffff.
## Changes
- Make the events module an optional feature `events` (to make crossterm more lightweight) (#776)
## Breaking ⚠️
- Set minimum rustc version to 1.58 (#798)
- Change all error types to `std::io::Result` (#765)
# Version 0.26.1
## Added ⭐
- Add synchronized output/update control (#756)
- Add kitty report alternate keys functionality (#754)
- Updates dev dependencies.
## Fixed 🐛
- Fix icorrect return in kitty keyboard enhancement check (#751)
- Fix panic when using `use-dev-tty` feature flag (#762)
# Version 0.26.0
## Added ⭐
- Add `SetCursorStyle` to set the cursor apearance and visibility. (#742)
- Add a function to check if kitty keyboard enhancement protocol is available. (#732)
- Add filedescriptors poll in order to move away from mio in the future (can be used via `use-dev-tty`). (#735)
## Fixed 🐛
- Improved F1-F4 handling for kitty keyboard protocol. (#736)
- Improved parsing of event types/modifiers with certain keys for kitty protocol. (#716)
## Breaking ⚠️
- Remove `SetCursorShape` in favour of `SetCursorStyle`. (#742)
- Make Windows resize event match `terminal::size` (#714)
- Rust 1.58 or later is now required.
- Add key release event for windows. (#745)
# Version 0.25.0
BREAKING: `Copy` trait is removed from `Event`, you can keep it by removing the "bracked-paste" feature flag. However this flag might be standardized in the future.
We removed the `Copy` from `Event` because the new `Paste` event, which contains a pasted string into the terminal, which is a non-copy string.
- Add ability to paste a string in into the terminal and fetch the pasted string via events (see `Event::Paste` and `EnableBracketedPaste `).
- Add support for functional key codes from kitty keyboard protocol. Try out by `PushKeyboardEnhancementFlags`. This protocol allows for:
- See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#modifiers
- Press, Repeat, Release event kinds.
- SUPER, HYPER, META modifiers.
- Media keycodes
- Right/left SHIFT, Control, Alt, Super, Hyper, Meta
- IsoLevel3Shift, IsoLevel5Shift
- Capslock, scroll lock, numlock
- Printscreen, pauze, menue, keyboard begin.
- Create `SetStyle` command to allow setting various styling in one command.
- Terminal Focus events (see `Event::FocusGained` and `Event::FocusLost`)
# Version 0.24.0
- Add DoubleUnderlined, Undercurled, Underdots the text, Underdotted, Underdashes, Underdashed attributes and allow coloring their foreground / background color.
- Fix windows unicode character parsing, this fixed various key combinations and support typing unicode characters.
- Consistency and better documentation on mouse cursor operations (BREAKING CHANGE).
- MoveTo, MoveToColumn, MoveToRow are 0-based. (left top most cell is 0,0). Moving like this is absolute
- MoveToNextLine, MoveToPreviousLine, MoveUp, MoveDown, MoveRight, MoveLeft are 1-based,. Moving like this is relative. Moving 1 left means moving 1 left. Moving 0 to the left is not possible, wikipedia states that most terminals will just default to 1.
- terminal::size returns error when previously it returned (0,0).
- Remove println from serialisation code.
- Fix mouse up for middle and right buttons.
- Fix escape codes on Git-Bash + Windows Terminal / Alacritty / WezTerm.
- Add support for cursor keys in application mode.
# Version 0.23.2
- Update signal-hook and mio to version 0.8.
# Version 0.23.1
- Fix control key parsing problem.
# Version 0.23
- Update dependencies.
- Add 0 check for all cursor functions to prevent undefined behaviour.
- Add CSIu key parsing for unix.
- Improve control character window key parsing supporting (e.g. CTRL [ and ])
- Update library to 2021 edition.
# Version 0.22.1
- Update yanked version crossterm-winapi and move to crossterm-winapi 0.9.0.
- Changed panic to error when calling disable-mouse capture without setting it first.
- Update bitflags dependency.
# Version 0.22
- Fix serde Color serialisation/deserialization inconsistency.
- Update crossterm-winapi 0.8.1 to fix panic for certain mouse events
# Version 0.21
- Expose `is_raw` function.
- Add 'purge' option on unix system, this clears the entire screen buffer.
- Improve serialisation for color enum values.
# Version 0.20
- Update from signal-hook with 'mio-feature flag' to signal-hook-mio 0.2.1.
- Manually implements Eq, PartialEq and Hash for KeyEvent improving equality checks and hash calculation.
- `crossterm::ErrorKind` to `io::Error`.
- Added Cursor Shape Support.
- Add support for function keys F13...F20.
- Support taking any Display in `SetTitle` command.
- Remove lazy_static dependency.
- Remove extra Clone bounds in the style module.
- Add `MoveToRow` command.
- Remove writer parameter from execute_winapi
# Version 0.19
- Use single thread for async event reader.
- Patch timeout handling for event polling this was not working correctly.
- Add unix support for more key combinations mainly complex ones with ALT/SHIFT/CTRL.
- Derive `PartialEq` and `Eq` for ContentStyle
- Fix windows resize event size, this used to be the buffer size but is screen size now.
- Change `Command::ansi_code` to `Command::write_ansi`, this way the ansi code will be written to given formatter.
# Version 0.18.2
- Fix panic when only setting bold and redirecting stdout.
- Use `tty_fd` for set/get terminal attributes
# Version 0.18.1
- Fix enabling ANSI support when stdout is redirected
- Update crossterm-winapi to 0.6.2
# Version 0.18.0
- Fix get position bug
- Fix windows 8 or lower write to user-given stdout instead of stdout.
- Make MoveCursor(Left/Right/Up/Dow) command with input 0 not move.
- Switch to futures-core to reduce dependencies.
- Command API restricts to only accept `std::io::Write`
- Make `supports_ansi` public
- Implement ALT + numbers windows systems.
# Version 0.17.7
- Fix cursor position retrieval bug linux.
# Version 0.17.6
- Add functionality to retrieve color based on passed ansi code.
- Switch from 'futures' to 'futures-util' crate to reduce dependency count
- Mio 0.7 update
- signal-hook update
- Make windows raw_mode act on CONIN$
- Added From<(u8, u8, u8)> Trait to Color::Rgb Enum
- Implement Color::try_from()
- Implement styler traits for `&'a str`
# Version 0.17.5
- Improved support of keymodifier for linux, arrow keys, function keys, home keys etc.
- Add `SetTitle` command to change the terminal title.
- Mio 0.7 update
# Version 0.17.4
- Add macros for `Colorize` and `Styler` impls, add an impl for `String`
- Add shift modifier to uppercase char events on unix
# Version 0.17.3
- Fix get terminal size mac os, this did not report the correct size.
# Version 0.17.2
- Windows unicode support
# Version 0.17.1
- Reverted bug in 0.17.0: "Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing.".
- Support for querying whether the current instance is a TTY.
# Version 0.17
- Impl Display for MoveToColumn, MoveToNextLine, MoveToPreviousLine
- Make unix event reader always use `/dev/tty`.
- Direct write command ansi_codes into formatter instead of double allocation.
- Add NONE flag to KeyModifiers
- Add support for converting chars to StylizedContent
- Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing.
# Version 0.16.0
- Change attribute vector in `ContentStyle` to bitmask.
- Add `SetAttributes` command.
- Add `Attributes` type, which is a bitfield of enabled attributes.
- Remove `exit()`, was useless.
# Version 0.15.0
- Fix CTRL + J key combination. This used to return an ENTER event.
- Add a generic implementation `Command` for `&T: Command`. This allows commands to be queued by reference, as well as by value.
- Remove unnecessary `Clone` trait bounds from `StyledContent`.
- Add `StyledContent::style_mut`.
- Handle error correctly for `execute!` and `queue!`.
- Fix minor syntax bug in `execute!` and `queue!`.
- Change `ContentStyle::apply` to take self by value instead of reference, to prevent an unnecessary extra clone.
- Added basic trait implementations (`Debug`, `Clone`, `Copy`, etc) to all of the command structs
- `ResetColor` uses `&'static str` instead of `String`
# Version 0.14.2
- Fix TIOCGWINSZ for FreeBSD
# Version 0.14.1
- Made windows cursor position relative to the window instead absolute to the screen buffer windows.
- Fix windows bug with `queue` macro were it consumed a type and required an type to be `Copy`.
# Version 0.14
- Replace the `input` module with brand new `event` module
- Terminal Resize Events
- Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and
- futures Stream (feature 'event-stream')
- Poll/read API
- It's **highly recommended** to read the
[Upgrade from 0.13 to 0.14](https://github.com/crossterm-rs/crossterm/wiki/Upgrade-from-0.13-to-0.14)
documentation
- Replace `docs/UPGRADE.md` with the [Upgrade Paths](https://github.com/crossterm-rs/crossterm/wiki#upgrade-paths)
documentation
- Add `MoveToColumn`, `MoveToPreviousLine`, `MoveToNextLine` commands
- Merge `screen` module into `terminal`
- Remove `screen::AlternateScreen`
- Remove `screen::Rawscreen`
* Move and rename `Rawscreen::into_raw_mode` and `Rawscreen::disable_raw_mode` to `terminal::enable_raw_mode` and `terminal::disable_raw_mode`
- Move `screen::EnterAlternateScreen` and `screen::LeaveAlternateScreen` to `terminal::EnterAlternateScreen` and `terminal::LeaveAlternateScreen`
- Replace `utils::Output` command with `style::Print` command
- Fix enable/disable mouse capture commands on Windows
- Allow trailing comma `queue!` & `execute!` macros
# Version 0.13.3
- Remove thread from AsyncReader on Windows.
- Improve HANDLE management windows.
# Version 0.13.2
- New `input::stop_reading_thread()` function
- Temporary workaround for the UNIX platform to stop the background
reading thread and close the file descriptor
- This function will be removed in the next version
# Version 0.13.1
- Async Reader fix, join background thread and avoid looping forever on windows.
# Version 0.13.0
**Major API-change, removed old-api**
- Remove `Crossterm` type
- Remove `TerminalCursor`, `TerminalColor`, `Terminal`
- Remove `cursor()`, `color()` , `terminal()`
- Remove re-exports at root, accessible via `module::types` (`cursor::MoveTo`)
- `input` module
- Derive 'Copy' for 'KeyEvent'
- Add the `EnableMouseCapture` and `EnableMouseCapture` commands
- `cursor` module
- Introduce static function `crossterm::cursor::position` in place of `TerminalCursor::pos`
- Rename `Goto` to `MoveTo`
- Rename `Up` to `MoveLeft`
- Rename `Right` to `MoveRight`
- Rename `Down` to `MoveDown`
- Rename `BlinkOn` to `EnableBlinking`
- Rename `BlinkOff` to `DisableBlinking`
- Rename `ResetPos` to `ResetPosition`
- Rename `SavePos` to `SavePosition`
- `terminal`
- Introduce static function `crossterm::terminal::size` in place of `Terminal::size`
- Introduce static function `crossterm::terminal::exit` in place of `Terminal::exit`
- `style module`
- Rename `ObjectStyle` to `ContentStyle`. Now full names are used for methods
- Rename `StyledObject` to `StyledContent` and made members private
- Rename `PrintStyledFont` to `PrintStyledContent`
- Rename `attr` method to `attribute`.
- Rename `Attribute::NoInverse` to `NoReverse`
- Update documentation
- Made `Colored` private, user should use commands instead
- Rename `SetFg` -> `SetForegroundColor`
- Rename `SetBg` -> `SetBackgroundColor`
- Rename `SetAttr` -> `SetAttribute`
- Rename `ContentStyle::fg_color` -> `ContentStyle::foreground_color`
- Rename `ContentStyle::bg_color` -> `ContentStyle::background_color`
- Rename `ContentStyle::attrs` -> `ContentStyle::attributes`
- Improve documentation
- Unix terminal size calculation with TPUT
# Version 0.12.1
- Move all the `crossterm_` crates code was moved to the `crossterm` crate
- `crossterm_cursor` is in the `cursor` module, etc.
- All these modules are public
- No public API breaking changes
# Version 0.12.0
- Following crates are deprecated and no longer maintained
- `crossterm_cursor`
- `crossterm_input`
- `crossterm_screen`
- `crossterm_style`
- `crossterm_terminal`
- `crossterm_utils`
## `crossterm_cursor` 0.4.0
- Fix examples link ([PR #6](https://github.com/crossterm-rs/crossterm-cursor/pull/6))
- Sync documentation style ([PR #7](https://github.com/crossterm-rs/crossterm-cursor/pull/7))
- Remove all references to the crossterm book ([PR #8](https://github.com/crossterm-rs/crossterm-cursor/pull/8))
- Replace `RAW_MODE_ENABLED` with `is_raw_mode_enabled` ([PR #9](https://github.com/crossterm-rs/crossterm-cursor/pull/9))
- Use `SyncReader` & `InputEvent::CursorPosition` for `pos_raw()` ([PR #10](https://github.com/crossterm-rs/crossterm-cursor/pull/10))
## `crossterm_input` 0.5.0
- Sync documentation style ([PR #4](https://github.com/crossterm-rs/crossterm-input/pull/4))
- Sync `SyncReader::next()` Windows and UNIX behavior ([PR #5](https://github.com/crossterm-rs/crossterm-input/pull/5))
- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-input/pull/6))
- Mouse coordinates synchronized with the cursor ([PR #7](https://github.com/crossterm-rs/crossterm-input/pull/7))
- Upper/left reported as `(0, 0)`
- Fix bug that read sync didn't block (Windows) ([PR #8](https://github.com/crossterm-rs/crossterm-input/pull/8))
- Refactor UNIX readers ([PR #9](https://github.com/crossterm-rs/crossterm-input/pull/9))
- AsyncReader produces mouse events
- One reading thread per application, not per `AsyncReader`
- Cursor position no longer consumed by another `AsyncReader`
- Implement sync reader for read_char (requires raw mode)
- Fix `SIGTTIN` when executed under the LLDB
- Add mio for reading from FD and more efficient polling (UNIX only)
- Sync UNIX and Windows vertical mouse position ([PR #11](https://github.com/crossterm-rs/crossterm-input/pull/11))
- Top is always reported as `0`
## `crossterm_screen` 0.3.2
- `to_alternate` switch back to main screen if it fails to switch into raw mode ([PR #4](https://github.com/crossterm-rs/crossterm-screen/pull/4))
- Improve the documentation ([PR #5](https://github.com/crossterm-rs/crossterm-screen/pull/5))
- Public API
- Include the book content in the documentation
- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-screen/pull/6))
- New commands introduced ([PR #7](https://github.com/crossterm-rs/crossterm-screen/pull/7))
- `EnterAlternateScreen`
- `LeaveAlternateScreen`
- Sync Windows and UNIX raw mode behavior ([PR #8](https://github.com/crossterm-rs/crossterm-screen/pull/8))
## `crossterm_style` 0.5.2
- Refactor ([PR #2](https://github.com/crossterm-rs/crossterm-style/pull/2))
- Added unit tests
- Improved documentation and added book page to `lib.rs`
- Fixed bug with `SetBg` command, WinApi logic
- Fixed bug with `StyledObject`, used stdout for resetting terminal color
- Introduced `ResetColor` command
- Sync documentation style ([PR #3](https://github.com/crossterm-rs/crossterm-style/pull/3))
- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-style/pull/4))
- Windows 7 grey/white foreground/intensity swapped ([PR #5](https://github.com/crossterm-rs/crossterm-style/pull/5))
## `crossterm_terminal` 0.3.2
- Removed `crossterm_cursor::sys` dependency ([PR #2](https://github.com/crossterm-rs/crossterm-terminal/pull/2))
- Internal refactoring & documentation ([PR #3](https://github.com/crossterm-rs/crossterm-terminal/pull/3))
- Removed all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-terminal/pull/4))
## `crossterm_utils` 0.4.0
- Add deprecation note ([PR #3](https://github.com/crossterm-rs/crossterm-utils/pull/3))
- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-utils/pull/4))
- Remove unsafe static mut ([PR #5](https://github.com/crossterm-rs/crossterm-utils/pull/5))
- `sys::unix::RAW_MODE_ENABLED` replaced with `sys::unix::is_raw_mode_enabled()` (breaking)
- New `lazy_static` dependency
## `crossterm_winapi` 0.3.0
- Make read sync block for windows systems ([PR #2](https://github.com/crossterm-rs/crossterm-winapi/pull/2))
# Version 0.11.1
- Maintenance release
- All sub-crates were moved to their own repositories in the `crossterm-rs` organization
# Version 0.11.0
As a preparation for crossterm 0.1.0 we have moved crossterm to an organisation called 'crossterm-rs'.
### Code Quality
- Code Cleanup: [warning-cleanup], [crossterm_style-cleanup], [crossterm_screen-cleanup], [crossterm_terminal-cleanup], [crossterm_utils-cleanup], [2018-cleanup], [api-cleanup-1], [api-cleanup-2], [api-cleanup-3]
- Examples: [example-cleanup_1], [example-cleanup_2], [example-fix], [commandbar-fix], [snake-game-improved]
- Fixed all broken tests and added tests
### Important Changes
- Return written bytes: [return-written-bytes]
- Added derives: `Debug` for `ObjectStyle` [debug-derive], Serialize/Deserialize for key events [serde]
- Improved error handling:
- Return `crossterm::Result` from all api's: [return_crossterm_result]
* `TerminalCursor::pos()` returns `Result<(u16, u16)>`
* `Terminal::size()` returns `Result<(u16, u16)>`
* `TerminalCursor::move_*` returns `crossterm::Result`
* `ExecutableCommand::queue` returns `crossterm::Result`
* `QueueableCommand::queue` returns `crossterm::Result`
* `get_available_color_count` returns no result
* `RawScreen::into_raw_mode` returns `crossterm::Result` instead of `io::Result`
* `RawScreen::disable_raw_mode` returns `crossterm::Result` instead of `io::Result`
* `AlternateScreen::to_alternate` returns `crossterm::Result` instead of `io::Result`
* `TerminalInput::read_line` returns `crossterm::Result` instead of `io::Result`
* `TerminalInput::read_char` returns `crossterm::Result` instead of `io::Result`
* Maybe I forgot something, a lot of functions have changed
- Removed all unwraps/expects from library
- Add KeyEvent::Enter and KeyEvent::Tab: [added-key-event-enter], [added-key-event-tab]
- Sync set/get terminal size behaviour: [fixed-get-set-terminal-size]
- Method renames:
* `AsyncReader::stop_reading()` to `stop()`
* `RawScreen::disable_raw_mode_on_drop` to `keep_raw_mode_on_drop`
* `TerminalCursor::reset_position()` to `restore_position()`
* `Command::get_anis_code()` to `ansi_code()`
* `available_color_count` to `available_color_count()`
* `Terminal::terminal_size` to `Terminal::size`
* `Console::get_handle` to `Console::handle`
- All `i16` values for indexing: set size, set cursor pos, scrolling synced to `u16` values
- Command API takes mutable self instead of self
[serde]: https://github.com/crossterm-rs/crossterm/pull/190
[debug-derive]: https://github.com/crossterm-rs/crossterm/pull/192
[example-fix]: https://github.com/crossterm-rs/crossterm/pull/193
[commandbar-fix]: https://github.com/crossterm-rs/crossterm/pull/204
[warning-cleanup]: https://github.com/crossterm-rs/crossterm/pull/198
[example-cleanup_1]: https://github.com/crossterm-rs/crossterm/pull/196
[example-cleanup_2]: https://github.com/crossterm-rs/crossterm/pull/225
[snake-game-improved]: https://github.com/crossterm-rs/crossterm/pull/231
[crossterm_style-cleanup]: https://github.com/crossterm-rs/crossterm/pull/208
[crossterm_screen-cleanup]: https://github.com/crossterm-rs/crossterm/pull/209
[crossterm_terminal-cleanup]: https://github.com/crossterm-rs/crossterm/pull/210
[crossterm_utils-cleanup]: https://github.com/crossterm-rs/crossterm/pull/211
[2018-cleanup]: https://github.com/crossterm-rs/crossterm/pull/222
[wild-card-cleanup]: https://github.com/crossterm-rs/crossterm/pull/224
[api-cleanup-1]: https://github.com/crossterm-rs/crossterm/pull/235
[api-cleanup-2]: https://github.com/crossterm-rs/crossterm/pull/238
[api-cleanup-3]: https://github.com/crossterm-rs/crossterm/pull/240
[return-written-bytes]: https://github.com/crossterm-rs/crossterm/pull/212
[return_crossterm_result]: https://github.com/crossterm-rs/crossterm/pull/232
[added-key-event-tab]: https://github.com/crossterm-rs/crossterm/pull/239
[added-key-event-enter]: https://github.com/crossterm-rs/crossterm/pull/236
[fixed-get-set-terminal-size]: https://github.com/crossterm-rs/crossterm/pull/242
# Version 0.10.1
# Version 0.10.0 ~ yanked
- Implement command API, to have better performance and more control over how and when commands are executed. [PR](https://github.com/crossterm-rs/crossterm/commit/1a60924abd462ab169b6706aab68f4cca31d7bc2), [issue](https://github.com/crossterm-rs/crossterm/issues/171)
- Fix showing, hiding cursor windows implementation
- Remove some of the parsing logic from windows keys to ansi codes to key events [PR](https://github.com/crossterm-rs/crossterm/commit/762c3a9b8e3d1fba87acde237f8ed09e74cd9ecd)
- Made terminal size 1-based [PR](https://github.com/crossterm-rs/crossterm/commit/d689d7e8ed46a335474b8262bd76f21feaaf0c50)
- Add some derives
# Version 0.9.6
- Copy for KeyEvent
- CTRL + Left, Down, Up, Right key support
- SHIFT + Left, Down, Up, Right key support
- Fixed UNIX cursor position bug [issue](https://github.com/crossterm-rs/crossterm/issues/140), [PR](https://github.com/crossterm-rs/crossterm/pull/152)
# Version 0.9.5
- Prefetch buffer size for more efficient windows input reads. [PR](https://github.com/crossterm-rs/crossterm/pull/144)
# Version 0.9.4
- Reset foreground and background color individually. [PR](https://github.com/crossterm-rs/crossterm/pull/138)
- Backtap input support. [PR](https://github.com/crossterm-rs/crossterm/pull/129)
- Corrected white/grey and added dark grey.
- Fixed getting cursor position with raw screen enabled. [PR](https://github.com/crossterm-rs/crossterm/pull/134)
- Removed one redundant stdout lock
# Version 0.9.3
- Removed println from `SyncReader`
## Version 0.9.2
- Terminal size linux was not 0-based
- Windows mouse input event position was 0-based and should be 1-based
- Result, ErrorKind are made re-exported
- Fixed some special key combination detections for UNIX systems
- Made FreeBSD compile
## Version 0.9.1
- Fixed libc compile error
## Version 0.9.0 (yanked)
This release is all about moving to a stabilized API for 1.0.
- Major refactor and cleanup.
- Improved performance;
- No locking when writing to stdout.
- UNIX doesn't have any dynamic dispatch anymore.
- Windows has improved the way to check if ANSI modes are enabled.
- Removed lot's of complex API calls: `from_screen`, `from_output`
- Removed `Arc<TerminalOutput>` from all internal Api's.
- Removed termios dependency for UNIX systems.
- Upgraded deps.
- Removed about 1000 lines of code
- `TerminalOutput`
- `Screen`
- unsafe code
- Some duplicated code introduced by a previous refactor.
- Raw modes UNIX systems improved
- Added `NoItalic` attribute
## Version 0.8.2
- Bug fix for sync reader UNIX.
## Version 0.8.1
- Added public re-exports for input.
# Version 0.8.0
- Introduced KeyEvents
- Introduced MouseEvents
- Upgraded crossterm_winapi 0.2
# Version 0.7.0
- Introduced more `Attributes`
- Introduced easier ways to style text [issue 87](https://github.com/crossterm-rs/crossterm/issues/87).
- Removed `ColorType` since it was unnecessary.
# Version 0.6.0
- Introduced feature flags; input, cursor, style, terminal, screen.
- All modules are moved to their own crate.
- Introduced crossterm workspace
- Less dependencies.
- Improved namespaces.
[PR 84](https://github.com/crossterm-rs/crossterm/pull/84)
# Version 0.5.5
- Error module is made public [PR 78](https://github.com/crossterm-rs/crossterm/pull/78).
# Version 0.5.4
- WinApi rewrite and correctly error handled [PR 67](https://github.com/crossterm-rs/crossterm/pull/67)
- Windows attribute support [PR 62](https://github.com/crossterm-rs/crossterm/pull/62)
- Readline bug fix windows systems [PR 62](https://github.com/crossterm-rs/crossterm/pull/62)
- Error handling improvement.
- General refactoring, all warnings removed.
- Documentation improvement.
# Version 0.5.1
- Documentation refactor.
- Fixed broken API documentation [PR 53](https://github.com/crossterm-rs/crossterm/pull/53).
# Version 0.5.0
- Added ability to pause the terminal [issue](https://github.com/crossterm-rs/crossterm/issues/39)
- RGB support for Windows 10 systems
- ANSI color value (255) color support
- More convenient API, no need to care about `Screen` unless working with when working with alternate or raw screen [PR](https://github.com/crossterm-rs/crossterm/pull/44)
- Implemented Display for styled object
# Version 0.4.3
- Fixed bug [issue 41](https://github.com/crossterm-rs/crossterm/issues/41)
# Version 0.4.2
- Added functionality to make a styled object writable to screen [issue 33](https://github.com/crossterm-rs/crossterm/issues/33)
- Added unit tests.
- Bugfix with getting terminal size unix.
- Bugfix with returning written bytes [pull request 31](https://github.com/crossterm-rs/crossterm/pull/31)
- removed methods calls: `as_any()` and `as_any_mut()` from `TerminalOutput`
# Version 0.4.1
- Fixed resizing of ansi terminal with and height where in the wrong order.
# Version 0.4.0
- Input support (read_line, read_char, read_async, read_until_async)
- Styling module improved
- Everything is multithreaded (`Send`, `Sync`)
- Performance enhancements: removed mutexes, removed state manager, removed context type removed unnecessarily RC types.
- Bug fix resetting console color.
- Bug fix whit undoing raw modes.
- More correct error handling.
- Overall command improvement.
- Overall refactor of code.
# Version 0.3.0
This version has some braking changes check [upgrade manual](UPGRADE%20Manual.md) for more information about what is changed.
I think you should not switch to version `0.3.0` if you aren't going to use the AlternateScreen feature.
Because you will have some work to get to the new version of crossterm depending on your situation.
Some Features crossterm 0.3.0
- Alternate Screen for windows and unix systems.
- Raw screen for unix and windows systems [Issue 5](https://github.com/crossterm-rs/crossterm/issues/5)..
- Hiding an showing the cursor.
- Control over blinking of the terminal cursor (only some terminals are supporting this).
- The terminal state will be set to its original state when process ends [issue7](https://github.com/crossterm-rs/crossterm/issues/7).
- exit the current process.
## Alternate screen
This create supports alternate screen for both windows and unix systems. You can use
*Nix style applications often utilize an alternate screen buffer, so that they can modify the entire contents of the buffer, without affecting the application that started them.
The alternate buffer is exactly the dimensions of the window, without any scrollback region.
For an example of this behavior, consider when vim is launched from bash.
Vim uses the entirety of the screen to edit the file, then returning to bash leaves the original buffer unchanged.
I Highly recommend you to check the `examples/program_examples/first_depth_search` for seeing this in action.
## Raw screen
This crate now supports raw screen for both windows and unix systems.
What exactly is raw state:
- No line buffering.
Normally the terminals uses line buffering. This means that the input will be send to the terminal line by line.
With raw mode the input will be send one byte at a time.
- Input
All input has to be written manually by the programmer.
- Characters
The characters are not processed by the terminal driver, but are sent straight through.
Special character have no meaning, like backspace will not be interpret as backspace but instead will be directly send to the terminal.
With these modes you can easier design the terminal screen.
## Some functionalities added
- Hiding and showing terminal cursor
- Enable or disabling blinking of the cursor for unix systems (this is not widely supported)
- Restoring the terminal to original modes.
- Added a [wrapper](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) for managing all the functionalities of crossterm `Crossterm`.
- Exit the current running process
## Examples
Added [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) for each version of the crossterm version.
Also added a folder with some [real life examples](https://github.com/crossterm-rs/crossterm/tree/master/examples/program_examples).
## Context
What is the `Context` all about? This `Context` has several reasons why it is introduced into `crossterm version 0.3.0`.
These points are related to the features like `Alternatescreen` and managing the terminal state.
- At first `Terminal state`:
Because this is a terminal manipulating library there will be made changes to terminal when running an process.
If you stop the process you want the terminal back in its original state.
Therefore, I need to track the changes made to the terminal.
- At second `Handle to the console`
In Rust we can use `stdout()` to get an handle to the current default console handle.
For example when in unix systems you want to print something to the main screen you can use the following code:
write!(std::io::stdout(), "{}", "some text").
But things change when we are in alternate screen modes.
We can not simply use `stdout()` to get a handle to the alternate screen, since this call returns the current default console handle (handle to mainscreen).
Because of that we need to store an handle to the current screen.
This handle could be used to put into alternate screen modes and back into main screen modes.
Through this stored handle Crossterm can execute its command and write on and to the current screen whether it be alternate screen or main screen.
For unix systems we store the handle gotten from `stdout()` for windows systems that are not supporting ANSI escape codes we store WinApi `HANDLE` struct witch will provide access to the current screen.
So to recap this `Context` struct is a wrapper for a type that manges terminal state changes.
When this `Context` goes out of scope all changes made will be undone.
Also is this `Context` is a wrapper for access to the current console screen.
Because Crossterm needs access to the above to types quite often I have chosen to add those two in one struct called `Context` so that this type could be shared throughout library.
Check this link for more info: [cleanup of rust code](https://stackoverflow.com/questions/48732387/how-can-i-run-clean-up-code-in-a-rust-library).
More info over writing to alternate screen buffer on windows and unix see this [link](https://github.com/crossterm-rs/crossterm/issues/17)
__Now the user has to pass an context type to the modules of Crossterm like this:__
let context = Context::new();
let cursor = cursor(&context);
let terminal = terminal(&context);
let color = color(&context);
Because this looks a little odd I will provide a type widths will manage the `Context` for you. You can call the different modules like the following:
let crossterm = Crossterm::new();
let color = crossterm.color();
let cursor = crossterm.cursor();
let terminal = crossterm.terminal();
### Alternate screen
When you want to switch to alternate screen there are a couple of things to keep in mind for it to work correctly.
First off some code of how to switch to Alternate screen, for more info check the [alternate screen example](https://github.com/crossterm-rs/crossterm/blob/master/examples/alternate_screen.rs).
_Create alternate screen from `Context`_
// create context.
let context = crossterm::Context::new();
// create instance of Alternatescreen by the given context, this will also switch to it.
let mut screen = crossterm::AlternateScreen::from(context.clone());
// write to the alternate screen.
write!(screen, "test");
_Create alternate screen from `Crossterm`:_
// create context.
let crossterm = ::crossterm::Crossterm::new();
// create instance of Alternatescreen by the given reference to crossterm, this will also switch to it.
let mut screen = crossterm::AlternateScreen::from(&crossterm);
// write to the alternate screen.
write!(screen, "test");
like demonstrated above, to get the functionalities of `cursor(), color(), terminal()` also working on alternate screen.
You need to pass it the same `Context` as you have passed to the previous three called functions,
If you don't use the same `Context` in `cursor(), color(), terminal()` than these modules will be using the main screen and you will not see anything at the alternate screen. If you use the [Crossterm](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) type you can get the `Context` from it by calling the crossterm.get_context() whereafter you can create the AlternateScreen from it.
# Version 0.2.2
- Bug see [issue 15](https://github.com/crossterm-rs/crossterm/issues/15)
# Version 0.2.1
- Default ANSI escape codes for windows machines, if windows does not support ANSI switch back to WinApi.
- method grammar mistake fixed [Issue 3](https://github.com/crossterm-rs/crossterm/issues/3)
- Some Refactorings in method names see [issue 4](https://github.com/crossterm-rs/crossterm/issues/4)
- Removed bin reference from crate [Issue 6](https://github.com/crossterm-rs/crossterm/issues/6)
- Get position unix fixed [issue 8](https://github.com/crossterm-rs/crossterm/issues/8)
# Version 0.2
- 256 color support.
- Text Attributes like: bold, italic, underscore and crossed word etc.
- Custom ANSI color code input to set fore- and background color for unix.
- Storing the current cursor position and resetting to that stored cursor position later.
- Resizing the terminal.

View File

@ -1,97 +0,0 @@
[package]
name = "keyfork-crossterm"
version = "0.27.1"
# authors = ["T. Post"]
authors = ["Ryan Heywood <ryan@distrust.co>"]
description = "A crossplatform terminal library for manipulating terminals."
repository = "https://git.distrust.co/public/keyfork"
# documentation = "https://docs.rs/crossterm/"
license = "MIT"
# keywords = ["event", "color", "cli", "input", "terminal"]
# exclude = ["target", "Cargo.lock"]
readme = "README.md"
edition = "2021"
rust-version = "1.58.0"
# categories = ["command-line-interface", "command-line-utilities"]
# [lib]
# name = "crossterm"
# path = "src/lib.rs"
# Build documentation with all features -> EventStream is available
[package.metadata.docs.rs]
all-features = true
# Features
[features]
default = ["bracketed-paste", "windows", "events"]
windows = ["dep:winapi", "dep:crossterm_winapi"] # Disables winapi dependencies from being included into the binary (SHOULD NOT be disabled on windows).
bracketed-paste = [] # Enables triggering a `Event::Paste` when pasting text into the terminal.
event-stream = ["dep:futures-core", "events"] # Enables async events
use-dev-tty = ["filedescriptor"] # Enables raw file descriptor polling / selecting instead of mio.
events = ["dep:mio", "dep:signal-hook", "dep:signal-hook-mio"] # Enables reading input/events from the system.
serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
# Shared dependencies
[dependencies]
bitflags = {version = "2.3" }
parking_lot = "0.12"
# optional deps only added when requested
futures-core = { version = "0.3", optional = true, default-features = false }
serde = { version = "1.0", features = ["derive"], optional = true }
# Windows dependencies
[target.'cfg(windows)'.dependencies.winapi]
version = "0.3.9"
features = ["winuser", "winerror"]
optional = true
[target.'cfg(windows)'.dependencies]
crossterm_winapi = { version = "0.9.1", optional = true }
# UNIX dependencies
[target.'cfg(unix)'.dependencies]
libc = "0.2"
signal-hook = { version = "0.3.17", optional = true }
filedescriptor = { version = "0.8", optional = true }
mio = { version = "0.8", features = ["os-poll"], optional = true }
signal-hook-mio = { version = "0.2.3", features = ["support-v0_8"], optional = true }
# Dev dependencies (examples, ...)
[dev-dependencies]
tokio = { version = "1.25", features = ["full"] }
futures = "0.3"
futures-timer = "3.0"
async-std = "1.12"
serde_json = "1.0"
serial_test = "2.0.0"
# Examples
[[example]]
name = "event-read"
required-features = ["bracketed-paste", "events"]
[[example]]
name = "event-match-modifiers"
required-features = ["bracketed-paste", "events"]
[[example]]
name = "event-poll-read"
required-features = ["bracketed-paste", "events"]
[[example]]
name = "event-stream-async-std"
required-features = ["event-stream", "events"]
[[example]]
name = "event-stream-tokio"
required-features = ["event-stream", "events"]
[[example]]
name = "event-read-char-line"
required-features = ["events"]
[[example]]
name = "stderr"
required-features = ["events"]

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Timon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1 +0,0 @@
Forked from https://github.com/crossterm-rs/crossterm

View File

@ -1,213 +0,0 @@
<h1 align="center"><img width="440" src="docs/crossterm_full.png" /></h1>
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2) ![Travis][s7] [![Latest Version][s1]][l1] [![MIT][s2]][l2] [![docs][s3]][l3] ![Lines of Code][s6] [![Join us on Discord][s5]][l5]
# Cross-platform Terminal Manipulation Library
Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested,
see [Tested Terminals](#tested-terminals) for more info).
## Table of Contents
- [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Tested Terminals](#tested-terminals)
- [Getting Started](#getting-started)
- [Feature Flags](#feature-flags)
- [Dependency Justification](#dependency-justification)
- [Other Resources](#other-resources)
- [Used By](#used-by)
- [Contributing](#contributing)
- [Authors](#authors)
- [License](#license)
## Features
- Cross-platform
- Multi-threaded (send, sync)
- Detailed documentation
- Few dependencies
- Full control over writing and flushing output buffer
- Is tty
- Cursor
- Move the cursor N times (up, down, left, right)
- Move to previous / next line
- Move to column
- Set/get the cursor position
- Store the cursor position and restore to it later
- Hide/show the cursor
- Enable/disable cursor blinking (not all terminals do support this feature)
- Styled output
- Foreground color (16 base colors)
- Background color (16 base colors)
- 256 (ANSI) color support (Windows 10 and UNIX only)
- RGB color support (Windows 10 and UNIX only)
- Text attributes like bold, italic, underscore, crossed, etc
- Terminal
- Clear (all lines, current line, from cursor down and up, until new line)
- Scroll up, down
- Set/get the terminal size
- Exit current process
- Alternate screen
- Raw screen
- Set terminal title
- Enable/disable line wrapping
- Event
- Input Events
- Mouse Events (press, release, position, button, drag)
- Terminal Resize Events
- Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and
- futures Stream (feature 'event-stream')
- Poll/read API
<!--
WARNING: Do not change following heading title as it's used in the URL by other crates!
-->
### Tested Terminals
- Console Host
- Windows 10 (Pro)
- Windows 8.1 (N)
- Ubuntu Desktop Terminal
- Ubuntu 17.10
- Pop!_OS ( Ubuntu ) 20.04
- (Arch, Manjaro) KDE Konsole
- (Arch, NixOS) Kitty
- Linux Mint
- (OpenSuse) Alacritty
- (Chrome OS) Crostini
- Apple
- macOS Monterey 12.7.1 (Intel-Chip)
This crate supports all UNIX terminals and Windows terminals down to Windows 7; however, not all of the
terminals have been tested. If you have used this library for a terminal other than the above list without
issues, then feel free to add it to the above list - I really would appreciate it!
## Getting Started
_see the [examples directory](examples/) and [documentation](https://docs.rs/crossterm/) for more advanced examples._
<details>
<summary>
Click to show Cargo.toml.
</summary>
```toml
[dependencies]
crossterm = "0.27"
```
</details>
<p></p>
```rust
use std::io::{stdout, Write};
use crossterm::{
execute,
style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
ExecutableCommand, Result,
event,
};
fn main() -> std::io::Result<()> {
// using the macro
execute!(
stdout(),
SetForegroundColor(Color::Blue),
SetBackgroundColor(Color::Red),
Print("Styled text here."),
ResetColor
)?;
// or using functions
stdout()
.execute(SetForegroundColor(Color::Blue))?
.execute(SetBackgroundColor(Color::Red))?
.execute(Print("Styled text here."))?
.execute(ResetColor)?;
Ok(())
}
```
Checkout this [list](https://docs.rs/crossterm/latest/crossterm/index.html#supported-commands) with all possible commands.
### Feature Flags
```toml
[dependencies.crossterm]
version = "0.27"
features = ["event-stream"]
```
| Feature | Description |
|:---------------|:---------------------------------------------|
| `event-stream` | `futures::Stream` producing `Result<Event>`. |
| `serde` | (De)serializing of events. |
| `events` | Reading input/system events (enabled by default) |
| `filedescriptor` | Use raw filedescriptor for all events rather then mio dependency |
To use crossterm as a very thin layer you can disable the `events` feature or use `filedescriptor` feature.
This can disable `mio` / `signal-hook` / `signal-hook-mio` dependencies.
### Dependency Justification
| Dependency | Used for | Included |
|:---------------|:---------------------------------------------------------------------------------|:--------------------------------------|
| `bitflags` | `KeyModifiers`, those are differ based on input. | always |
| `parking_lot` | locking `RwLock`s with a timeout, const mutexes. | always |
| `libc` | UNIX terminal_size/raw modes/set_title and several other low level functionality. | optional (`events` feature), UNIX only |
| `Mio` | event readiness polling, waking up poller | optional (`events` feature), UNIX only |
| `signal-hook` | signal-hook is used to handle terminal resize SIGNAL with Mio. | optional (`events` feature),UNIX only |
| `winapi` | Used for low-level windows system calls which ANSI codes can't replace | windows only |
| `futures-core` | For async stream of events | only with `event-stream` feature flag |
| `serde` | ***ser***ializing and ***de***serializing of events | only with `serde` feature flag |
### Other Resources
- [API documentation](https://docs.rs/crossterm/)
- [Deprecated examples repository](https://github.com/crossterm-rs/examples)
## Used By
- [Broot](https://dystroy.org/broot/)
- [Cursive](https://github.com/gyscos/Cursive)
- [TUI](https://github.com/fdehau/tui-rs)
- [Rust-sloth](https://github.com/ecumene/rust-sloth)
- [Rusty-rain](https://github.com/cowboy8625/rusty-rain)
## Contributing
We highly appreciate when anyone contributes to this crate. Before you do, please,
read the [Contributing](docs/CONTRIBUTING.md) guidelines.
## Authors
* **Timon Post** - *Project Owner & creator*
## License
This project, `crossterm` and all its sub-crates: `crossterm_screen`, `crossterm_cursor`, `crossterm_style`,
`crossterm_input`, `crossterm_terminal`, `crossterm_winapi`, `crossterm_utils` are licensed under the MIT
License - see the [LICENSE](https://github.com/crossterm-rs/crossterm/blob/master/LICENSE) file for details.
[s1]: https://img.shields.io/crates/v/crossterm.svg
[l1]: https://crates.io/crates/crossterm
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
[l2]: ./LICENSE
[s3]: https://docs.rs/crossterm/badge.svg
[l3]: https://docs.rs/crossterm/
[s3]: https://docs.rs/crossterm/badge.svg
[l3]: https://docs.rs/crossterm/
[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord
[l5]: https://discord.gg/K4nyTDB
[s6]: https://tokei.rs/b1/github/crossterm-rs/crossterm?category=code
[s7]: https://travis-ci.org/crossterm-rs/crossterm.svg?branch=master

View File

@ -1 +0,0 @@
book

View File

@ -1,65 +0,0 @@
# Contributing
I would appreciate any contributions to this crate. However, some things are handy to know.
## Code Style
### Import Order
All imports are semantically grouped and ordered. The order is:
- standard library (`use std::...`)
- external crates (`use rand::...`)
- current crate (`use crate::...`)
- parent module (`use super::..`)
- current module (`use self::...`)
- module declaration (`mod ...`)
There must be an empty line between groups. An example:
```rust
use crossterm_utils::{csi, write_cout, Result};
use crate::sys::{get_cursor_position, show_cursor};
use super::Cursor;
```
#### CLion Tips
The CLion IDE does this for you (_Menu_ -> _Code_ -> _Optimize Imports_). Be aware that the CLion sorts
imports in a group in a different way when compared to the `rustfmt`. It's effectively two steps operation
to get proper grouping & sorting:
* _Menu_ -> _Code_ -> _Optimize Imports_ - group & semantically order imports
* `cargo fmt` - fix ordering within the group
Second step can be automated via _CLion_ -> _Preferences_ ->
_Languages & Frameworks_ -> _Rust_ -> _Rustfmt_ -> _Run rustfmt on save_.
### Max Line Length
| Type | Max line length |
|:---------------------|----------------:|
| Code | 100 |
| Comments in the code | 120 |
| Documentation | 120 |
100 is the [`max_width`](https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#max_width)
default value.
120 is because of the GitHub. The editor & viewer width there is +- 123 characters.
### Warnings
The code must be warning free. It's quite hard to find an error if the build logs are polluted with warnings.
If you decide to silent a warning with (`#[allow(...)]`), please add a comment why it's required.
Always consult the [Travis CI](https://travis-ci.org/crossterm-rs/crossterm/pull_requests) build logs.
### Forbidden Warnings
Search for `#![deny(...)]` in the code:
* `unused_must_use`
* `unused_imports`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Some files were not shown because too many files have changed in this diff Show More