Compare commits

...

16 Commits

Author SHA1 Message Date
Ryan Heywood ee258ac115
keyforkd: allow sending server-side error to clients 2023-11-05 23:21:10 -06:00
Ryan Heywood ada6cf150b
keyforkd: split into enum based request 2023-11-05 22:51:40 -06:00
Ryan Heywood 48e4d7096c
keyfork-shard: remove recovery file 2023-11-05 16:34:45 -06:00
Ryan Heywood 0ec9f9c567
keyfork-shard: cleanup eprintln 2023-11-05 16:26:19 -06:00
Ryan Heywood fa61d31f3f
keyfork-shard: further improve error handling, add multiline prompt and PIN retry detection 2023-11-05 16:21:54 -06:00
Ryan Heywood baa289ce62
keyfork-pinentry: use multiline prompts at least for passphrase 2023-11-05 16:21:16 -06:00
Ryan Heywood 2c9d09ea61
keyfork: use better error handling 2023-11-05 16:21:02 -06:00
Ryan Heywood 307941087a
keyfork-shard: slightly improved error handling 2023-11-05 13:57:22 -06:00
Ryan Heywood 0768339487
keyfork-shard: parse cardholder name when using smartcards 2023-11-05 01:36:12 -05:00
Ryan Heywood 4b4b85931f
cargo fmt 2023-11-05 01:29:10 -05:00
Ryan Heywood c206800ad2
keyfork-shard: add keyfork-pinentry 2023-11-05 00:48:30 -05:00
Ryan Heywood 7f90e4ada4
keyfork-shard: fixup user PIN prompt 2023-11-04 12:19:53 -05:00
Ryan Heywood 726b62b3f4
keyfork-shard: make recovery file required, key discovery optional 2023-11-03 20:58:51 -05:00
Ryan Heywood 5b427516c6
keyfork-shard: enable step 1 decoding with openpgp-card, fix bug with multiple smartcards when decrypting 2023-11-03 20:42:33 -05:00
Ryan Heywood a184c62f42
keyfork-shard: ah, whitespace, why are you so cruel 2023-11-02 01:12:28 -05:00
Ryan Heywood adad3e5b6b
keyfork-shard: begin work on OpenPGP card support 2023-11-02 01:01:34 -05:00
35 changed files with 2199 additions and 386 deletions

537
Cargo.lock generated
View File

@ -32,6 +32,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.5.0"
@ -218,9 +233,9 @@ dependencies = [
[[package]]
name = "buffered-reader"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d3bea5bcc3ecc38fe5388e6bc35e6fe7bd665eb3ae9a44283e15b91ad3867d"
checksum = "2b9b0a25eb06e83579bc985d836e1e3b957a7201301b48538764d2b2e78090d4"
dependencies = [
"bzip2",
"flate2",
@ -234,6 +249,12 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.4.0"
@ -261,6 +282,27 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "card-backend"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd3ee3a298842065dc489180c34a4fe4bbbb8643bb422009d79558a099fb42e5"
dependencies = [
"thiserror",
]
[[package]]
name = "card-backend-pcsc"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68bb0b707b1b6b058ed93abd70ef65703ed6fd4150d32a0d735b78cfa61cbb35"
dependencies = [
"card-backend",
"iso7816-tlv",
"log",
"pcsc",
]
[[package]]
name = "cc"
version = "1.0.83"
@ -291,9 +333,12 @@ version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.48.5",
]
[[package]]
@ -372,6 +417,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.9"
@ -427,7 +478,7 @@ dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest 0.10.7",
"digest",
"fiat-crypto",
"platforms",
"rustc_version",
@ -446,6 +497,17 @@ dependencies = [
"syn 2.0.29",
]
[[package]]
name = "der"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
[[package]]
name = "der"
version = "0.7.8"
@ -462,15 +524,6 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -478,6 +531,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
@ -515,19 +569,10 @@ version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
dependencies = [
"der",
"der 0.7.8",
"elliptic-curve",
"signature 2.1.0",
"spki",
]
[[package]]
name = "ed25519"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
dependencies = [
"signature 1.6.4",
"signature",
"spki 0.7.2",
]
[[package]]
@ -536,8 +581,8 @@ version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d"
dependencies = [
"pkcs8",
"signature 2.1.0",
"pkcs8 0.10.2",
"signature",
]
[[package]]
@ -547,7 +592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
dependencies = [
"curve25519-dalek",
"ed25519 2.2.2",
"ed25519",
"serde",
"sha2",
"zeroize",
@ -567,11 +612,11 @@ checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b"
dependencies = [
"base16ct",
"crypto-bigint",
"digest 0.10.7",
"digest",
"ff",
"generic-array",
"group",
"pkcs8",
"pkcs8 0.10.2",
"rand_core 0.6.4",
"sec1",
"subtle",
@ -699,19 +744,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.2.10"
@ -721,7 +753,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"wasm-bindgen",
]
@ -787,20 +819,58 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "hex-slice"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5491a308e0214554f07a81d8944abe45f552871c12e3c3c6e7e5d354039a6c4c"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
"digest",
]
[[package]]
name = "home"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "iana-time-zone"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "idna"
version = "0.3.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@ -851,6 +921,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "iso7816-tlv"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d47365efc3b4c252f8a3384445c0f7e8a4e0ae5c22bf3bedd2dd16f9bb45016a"
dependencies = [
"untrusted",
]
[[package]]
name = "itertools"
version = "0.10.5"
@ -932,7 +1011,7 @@ dependencies = [
name = "keyfork-derive-util"
version = "0.1.0"
dependencies = [
"digest 0.10.7",
"digest",
"ed25519-dalek",
"hex-literal",
"hmac",
@ -968,6 +1047,18 @@ dependencies = [
"sha2",
]
[[package]]
name = "keyfork-pinentry"
version = "0.5.0"
dependencies = [
"nom",
"percent-encoding",
"secrecy",
"thiserror",
"which",
"zeroize",
]
[[package]]
name = "keyfork-plumbing"
version = "0.1.0"
@ -982,11 +1073,16 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"card-backend",
"card-backend-pcsc",
"keyfork-derive-openpgp",
"keyfork-pinentry",
"openpgp-card-sequoia",
"sequoia-openpgp",
"serde",
"sharks",
"smex",
"thiserror",
]
[[package]]
@ -1007,6 +1103,7 @@ dependencies = [
"keyfork-frame",
"keyfork-mnemonic-util",
"keyfork-slip10-test-data",
"keyforkd-models",
"serde",
"thiserror",
"tokio",
@ -1025,16 +1122,26 @@ dependencies = [
"keyfork-frame",
"keyfork-slip10-test-data",
"keyforkd",
"keyforkd-models",
"tempdir",
"thiserror",
"tokio",
]
[[package]]
name = "keyforkd-models"
version = "0.1.0"
dependencies = [
"keyfork-derive-util",
"serde",
"thiserror",
]
[[package]]
name = "lalrpop"
version = "0.19.12"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b"
checksum = "da4081d44f4611b66c6dd725e6de3169f9f63905421e8626fcb86b6a898998b8"
dependencies = [
"ascii-canvas",
"bit-set",
@ -1045,7 +1152,7 @@ dependencies = [
"lalrpop-util",
"petgraph",
"regex",
"regex-syntax 0.6.29",
"regex-syntax 0.7.4",
"string_cache",
"term",
"tiny-keccak",
@ -1054,15 +1161,18 @@ dependencies = [
[[package]]
name = "lalrpop-util"
version = "0.19.12"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed"
checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
dependencies = [
"spin",
]
[[package]]
name = "lazycell"
@ -1086,6 +1196,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "libm"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@ -1163,7 +1279,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"windows-sys 0.48.0",
]
@ -1173,7 +1289,7 @@ version = "7.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9fdccf3eae7b161910d2daa2f0155ca35041322e8fe5c5f1f2c9d0b12356336"
dependencies = [
"getrandom 0.2.10",
"getrandom",
"libc",
"nettle-sys",
"thiserror",
@ -1220,6 +1336,44 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.16"
@ -1227,6 +1381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -1254,6 +1409,36 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "openpgp-card"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba6b39b46a9deba985be9cc960709e284806b550d7e1aff915f8be4b06c3640"
dependencies = [
"card-backend",
"chrono",
"hex-slice",
"log",
"nom",
"thiserror",
]
[[package]]
name = "openpgp-card-sequoia"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7637080b15379df16fef0f81fd2664d403366b7514c721f2231c8974778017c3"
dependencies = [
"anyhow",
"card-backend",
"chrono",
"log",
"openpgp-card",
"rsa",
"sequoia-openpgp",
"thiserror",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -1289,16 +1474,50 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest 0.10.7",
"digest",
"hmac",
]
[[package]]
name = "pcsc"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37cab0be9d04e808a8d8059fa54befcd71dc8b168f9f0c04bdb7e59832abbab4"
dependencies = [
"bitflags 1.3.2",
"pcsc-sys",
]
[[package]]
name = "pcsc-sys"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b7bfecba2c0f1b5efb0e7caf7533ab1c295024165bcbb066231f60d33e23ea"
dependencies = [
"pkg-config",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "pem-rfc7468"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "petgraph"
version = "0.6.4"
@ -1350,14 +1569,36 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719"
dependencies = [
"der 0.6.1",
"pkcs8 0.9.0",
"spki 0.6.0",
"zeroize",
]
[[package]]
name = "pkcs8"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
dependencies = [
"der 0.6.1",
"spki 0.6.0",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
"der 0.7.8",
"spki 0.7.2",
]
[[package]]
@ -1415,19 +1656,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
]
[[package]]
name = "rand"
version = "0.8.5"
@ -1435,20 +1663,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_chacha",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
@ -1474,31 +1692,13 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.10",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
"getrandom",
]
[[package]]
@ -1534,7 +1734,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom 0.2.10",
"getrandom",
"redox_syscall 0.2.16",
"thiserror",
]
@ -1583,6 +1783,12 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
@ -1598,7 +1804,27 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
dependencies = [
"digest 0.10.7",
"digest",
]
[[package]]
name = "rsa"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4"
dependencies = [
"byteorder",
"digest",
"num-bigint-dig",
"num-integer",
"num-iter",
"num-traits",
"pkcs1",
"pkcs8 0.9.0",
"rand_core 0.6.4",
"signature",
"subtle",
"zeroize",
]
[[package]]
@ -1674,13 +1900,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"der 0.7.8",
"generic-array",
"pkcs8",
"pkcs8 0.10.2",
"subtle",
"zeroize",
]
[[package]]
name = "secrecy"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
dependencies = [
"zeroize",
]
[[package]]
name = "semver"
version = "1.0.18"
@ -1689,9 +1924,9 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "sequoia-openpgp"
version = "1.16.1"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a16854c0f6297de6db4df195e28324dfbc2429802f0e48cd04007db8e3049709"
checksum = "2ea026cf8a70d331c742e3ad7e68fd405d0743ff86630fb4334a1bf8d0e194c7"
dependencies = [
"anyhow",
"base64",
@ -1699,9 +1934,9 @@ dependencies = [
"bzip2",
"chrono",
"dyn-clone",
"ed25519 1.5.3",
"ed25519",
"flate2",
"getrandom 0.2.10",
"getrandom",
"idna",
"lalrpop",
"lalrpop-util",
@ -1710,9 +1945,9 @@ dependencies = [
"memsec",
"nettle",
"once_cell",
"rand 0.7.3",
"rand 0.8.5",
"regex",
"regex-syntax 0.6.29",
"regex-syntax 0.8.2",
"sha1collisiondetection",
"thiserror",
"xxhash-rust",
@ -1720,18 +1955,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.188"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.188"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
dependencies = [
"proc-macro2",
"quote",
@ -1751,11 +1986,11 @@ dependencies = [
[[package]]
name = "sha1collisiondetection"
version = "0.2.7"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b20793cf8330b2c7da4c438116660fed24e380bcb8a1bcfff2581b5593a0b38e"
checksum = "31c0b86a052106b16741199985c9ec2bf501f619f70c48fa479b44b093ad9a68"
dependencies = [
"digest 0.9.0",
"digest",
"generic-array",
]
@ -1767,7 +2002,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.7",
"digest",
]
[[package]]
@ -1805,18 +2040,13 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
[[package]]
name = "signature"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
dependencies = [
"digest",
"rand_core 0.6.4",
]
@ -1852,6 +2082,22 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spki"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
dependencies = [
"base64ct",
"der 0.6.1",
]
[[package]]
name = "spki"
version = "0.7.2"
@ -1859,7 +2105,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
dependencies = [
"base64ct",
"der",
"der 0.7.8",
]
[[package]]
@ -1955,18 +2201,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.49"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.49"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
dependencies = [
"proc-macro2",
"quote",
@ -2171,6 +2417,12 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "utf8parse"
version = "0.2.1"
@ -2195,12 +2447,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -2261,6 +2507,18 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "which"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.13",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -2283,6 +2541,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.45.0"

View File

@ -9,10 +9,12 @@ members = [
"keyfork-derive-util",
"keyfork-frame",
"keyfork-mnemonic-util",
"keyfork-pinentry",
"keyfork-plumbing",
"keyfork-shard",
"keyfork-slip10-test-data",
"keyforkd",
"keyforkd-client",
"keyforkd-models",
"smex",
]

View File

@ -1,7 +1,7 @@
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationError, DerivationRequest},
request::{DerivationAlgorithm, DerivationError, DerivationRequest, DerivationResponse},
DerivationPath,
};
use keyforkd_client::Client;
@ -38,8 +38,8 @@ 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)?;
println!("{}", smex::encode(&response.data));
let response = client.request(&request.into())?;
println!("{}", smex::encode(&DerivationResponse::try_from(response)?.data));
Ok(())
}

View File

@ -1,7 +1,7 @@
use std::{env, str::FromStr, process::ExitCode};
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest},
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
DerivationIndex, DerivationPath,
};
use keyforkd_client::Client;
@ -107,7 +107,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
};
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let derived_data = Client::discover_socket()?.request(&request)?;
let derived_data: DerivationResponse = Client::discover_socket()?
.request(&request.into())?
.try_into()?;
let subkeys = subkey_format
.iter()
.map(|kt| kt.inner().clone())

View File

@ -1,4 +1,4 @@
use keyfork_derive_util::{DerivationPath, DerivationIndex};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true);

View File

@ -1,4 +1,4 @@
use crate::{DerivationIndex, DerivationPath, PrivateKey, PublicKey, ExtendedPublicKey};
use crate::{DerivationIndex, DerivationPath, ExtendedPublicKey, PrivateKey, PublicKey};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};

View File

@ -122,11 +122,14 @@ mod tests {
Ok(())
}
#[test]
fn add_vec() -> Result<(), Box<dyn std::error::Error>> {
let path = DerivationPath::from_str("m")?;
let other_path = [DerivationIndex::new(72, true)?, DerivationIndex::new(47, false)?, DerivationIndex::new((i32::MAX) as u32, false)?];
let other_path = [
DerivationIndex::new(72, true)?,
DerivationIndex::new(47, false)?,
DerivationIndex::new((i32::MAX) as u32, false)?,
];
let path = path + &other_path[..];
assert_eq!(path, DerivationPath::from_str("m/72'/47/2147483647")?);

View File

@ -35,7 +35,9 @@ pub trait PublicKey: Sized {
let hash = Sha256::new().chain_update(self.to_bytes()).finalize();
let hash = Ripemd160::new().chain_update(hash).finalize();
// Note: Safety assured by type returned from Ripemd160
hash[..4].try_into().expect("Ripemd160 returned too little data")
hash[..4]
.try_into()
.expect("Ripemd160 returned too little data")
}
}

View File

@ -59,10 +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".to_string()).unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -110,7 +107,8 @@ fn panics_with_unhardened_derivation() {
let seed = hex!("000102030405060708090a0b0c0d0e0f");
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
xkey.derive_path(&DerivationPath::from_str("m/0").unwrap()).unwrap();
xkey.derive_path(&DerivationPath::from_str("m/0").unwrap())
.unwrap();
}
#[cfg(feature = "ed25519")]

View File

@ -1,8 +1,8 @@
use std::{collections::HashMap, str::FromStr, sync::Arc, error::Error, fmt::Display};
use std::{collections::HashMap, error::Error, fmt::Display, str::FromStr, sync::Arc};
use sha2::{Digest, Sha256, Sha512};
use pbkdf2::pbkdf2;
use hmac::Hmac;
use pbkdf2::pbkdf2;
use sha2::{Digest, Sha256, Sha512};
/// The error type representing a failure to create a [`Mnemonic`]. These errors only occur during
/// [`Mnemonic`] creation.
@ -30,7 +30,7 @@ impl Display for MnemonicGenerationError {
}
MnemonicGenerationError::InvalidPbkdf2Length => {
f.write_str("Invalid length from PBKDF2")
},
}
}
}
}
@ -207,7 +207,7 @@ impl Mnemonic {
bits[index * 11 + bit] = (word & (1 << (10 - bit))) > 0;
}
}
// remove checksum bits
bits.truncate(bits.len() - bits.len() % 32);
@ -222,7 +222,10 @@ impl Mnemonic {
.collect()
}
pub fn seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> Result<Vec<u8>, MnemonicGenerationError> {
pub fn seed<'a>(
&self,
passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> {
let passphrase = passphrase.into();
let mut seed = [0u8; 64];
@ -293,8 +296,14 @@ mod tests {
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
assert_eq!(my_mnemonic.seed(None).unwrap(), their_mnemonic.to_seed(""));
assert_eq!(my_mnemonic.seed("testing").unwrap(), their_mnemonic.to_seed("testing"));
assert_ne!(my_mnemonic.seed("test1").unwrap(), their_mnemonic.to_seed("test2"));
assert_eq!(
my_mnemonic.seed("testing").unwrap(),
their_mnemonic.to_seed("testing")
);
assert_ne!(
my_mnemonic.seed("test1").unwrap(),
their_mnemonic.to_seed("test2")
);
}
#[test]

View File

@ -0,0 +1,23 @@
[package]
# name = "pinentry"
name = "keyfork-pinentry"
description = "API for interacting with pinentry binaries"
version = "0.5.0"
# authors = ["Jack Grigg <thestr4d@gmail.com>"]
authors = ["Ryan Heywood <ryan@distrust.co>"]
# repository = "https://github.com/str4d/pinentry-rs"
# readme = "README.md"
# keywords = ["passphrase", "password"]
# categories = ["api-bindings", "command-line-interface"]
# license = "MIT OR Apache-2.0"
license = "MIT"
# edition = "2018"
edition = "2021"
[dependencies]
nom = { version = "7", default-features = false }
percent-encoding = "2.1"
secrecy = "0.8"
thiserror = "1.0.50"
which = { version = "4", default-features = false }
zeroize = "1"

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2020 Jack Grigg
Copyright (c) 2023 Distrust
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

@ -0,0 +1,225 @@
use percent_encoding::percent_decode_str;
use secrecy::{ExposeSecret, SecretString};
use std::borrow::Cow;
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;
use std::process::{ChildStdin, ChildStdout};
use std::process::{Command, Stdio};
use zeroize::Zeroize;
use crate::{Error, Result};
/// Possible response lines from an Assuan server.
///
/// Reference: https://gnupg.org/documentation/manuals/assuan/Server-responses.html
#[allow(dead_code)]
#[derive(Debug)]
enum Response {
/// Request was successful.
Ok(Option<String>),
/// Request could not be fulfilled. The possible error codes are defined by
/// `libgpg-error`.
Err {
code: u16,
description: Option<String>,
},
/// Informational output by the server, which is still processing the request.
Information {
keyword: String,
status: Option<String>,
},
/// Comment line issued only for debugging purposes.
Comment(String),
/// Raw data returned to client.
DataLine(SecretString),
/// The server needs further information from the client.
Inquire {
keyword: String,
parameters: Option<String>,
},
}
pub struct Connection {
output: ChildStdin,
input: BufReader<ChildStdout>,
}
impl Connection {
pub fn open(name: &Path) -> Result<Self> {
let process = Command::new(name)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let output = process.stdin.expect("could open stdin");
let input = BufReader::new(process.stdout.expect("could open stdin"));
let mut conn = Connection { output, input };
// There is always an initial OK server response
conn.read_response()?;
#[cfg(unix)]
{
conn.send_request("OPTION", Some("ttyname=/dev/tty"))?;
conn.send_request(
"OPTION",
Some(&format!(
"ttytype={}",
std::env::var("TERM")
.as_ref()
.map(|s| s.as_str())
.unwrap_or("xterm-256color")
)),
)?;
}
Ok(conn)
}
pub fn send_request(
&mut self,
command: &str,
parameters: Option<&str>,
) -> Result<Option<SecretString>> {
self.output.write_all(command.as_bytes())?;
if let Some(p) = parameters {
self.output.write_all(b" ")?;
self.output.write_all(p.as_bytes())?;
}
self.output.write_all(b"\n")?;
self.read_response()
}
fn read_response(&mut self) -> Result<Option<SecretString>> {
let mut line = String::new();
let mut data = None;
// We loop until we find an OK or ERR response. This is probably sufficient for
// pinentry, but other Assuan protocols might rely on INQUIRE, which needs
// intermediate completion states or callbacks.
loop {
line.zeroize();
self.input.read_line(&mut line)?;
match read::server_response(&line)
.map(|(_, r)| r)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?
{
Response::Ok(_info) => {
/*
if let Some(info) = info {
debug!("< OK {}", info);
}
*/
line.zeroize();
return Ok(data.map(SecretString::new));
}
Response::Err { code, description } => {
line.zeroize();
if let Some(mut buf) = data {
buf.zeroize();
}
return Err(Error::from_parts(code, description));
}
Response::Comment(_comment) => {
// debug!("< # {}", comment)
}
Response::DataLine(data_line) => {
let buf = data.take();
let data_line_decoded =
percent_decode_str(data_line.expose_secret()).decode_utf8()?;
data = Some(buf.unwrap_or_else(String::new) + &data_line_decoded);
if let Cow::Owned(mut data_line_decoded) = data_line_decoded {
data_line_decoded.zeroize();
}
}
_res => {
// info!("< {:?}", res)
}
}
}
}
}
impl Drop for Connection {
fn drop(&mut self) {
let _ = self.send_request("BYE", None);
}
}
mod read {
use nom::{
branch::alt,
bytes::complete::{is_not, tag},
character::complete::{digit1, line_ending},
combinator::{map, opt},
sequence::{pair, preceded, terminated},
IResult,
};
use secrecy::SecretString;
use super::Response;
fn gpg_error_code(input: &str) -> IResult<&str, u16> {
map(digit1, |code| {
#[allow(clippy::from_str_radix_10)]
let full = u32::from_str_radix(code, 10).expect("have decimal digits");
// gpg uses the lowest 16 bits for error codes.
full as u16
})(input)
}
pub(super) fn server_response(input: &str) -> IResult<&str, Response> {
terminated(
alt((
preceded(
tag("OK"),
map(opt(preceded(tag(" "), is_not("\r\n"))), |params| {
Response::Ok(params.map(String::from))
}),
),
preceded(
tag("ERR "),
map(
pair(gpg_error_code, opt(preceded(tag(" "), is_not("\r\n")))),
|(code, description)| Response::Err {
code,
description: description.map(String::from),
},
),
),
preceded(
tag("S "),
map(
pair(is_not(" \r\n"), opt(preceded(tag(" "), is_not("\r\n")))),
|(keyword, status): (&str, _)| Response::Information {
keyword: keyword.to_owned(),
status: status.map(String::from),
},
),
),
preceded(
tag("# "),
map(is_not("\r\n"), |comment: &str| {
Response::Comment(comment.to_owned())
}),
),
preceded(
tag("D "),
map(is_not("\r\n"), |data: &str| {
Response::DataLine(SecretString::new(data.to_owned()))
}),
),
preceded(
tag("INQUIRE "),
map(
pair(is_not(" \r\n"), opt(preceded(tag(" "), is_not("\r\n")))),
|(keyword, parameters): (&str, _)| Response::Inquire {
keyword: keyword.to_owned(),
parameters: parameters.map(String::from),
},
),
),
)),
line_ending,
)(input)
}
}

View File

@ -0,0 +1,114 @@
use std::{fmt, io};
pub(crate) const GPG_ERR_TIMEOUT: u16 = 62;
pub(crate) const GPG_ERR_CANCELED: u16 = 99;
pub(crate) const GPG_ERR_NOT_CONFIRMED: u16 = 114;
/// An uncommon or unexpected GPG error.
///
/// `pinentry` is built on top of Assuan, which inherits all of GPG's error codes. Only
/// some of these error codes are actually used by the common `pinentry` implementations,
/// but it's possible to receive any of them.
#[derive(Debug)]
pub struct GpgError {
/// The GPG error code.
///
/// See <https://github.com/gpg/libgpg-error/blob/master/src/err-codes.h.in> for the
/// mapping from error code to GPG error type.
code: u16,
/// A description of the error, if available.
///
/// See <https://github.com/gpg/libgpg-error/blob/master/src/err-codes.h.in> for the
/// likely descriptions.
description: Option<String>,
}
impl fmt::Display for GpgError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Code {}", self.code)?;
if let Some(desc) = &self.description {
write!(f, ": {}", desc)?;
}
Ok(())
}
}
impl std::error::Error for GpgError {}
impl GpgError {
pub(super) fn new(code: u16, description: Option<String>) -> Self {
GpgError { code, description }
}
/// Returns the GPG code for this error.
pub fn code(&self) -> u16 {
self.code
}
}
/// Errors that may be returned while interacting with `pinentry` binaries.
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// The user cancelled the operation.
#[error("The user cancelled the operation")]
Cancelled,
/// Operation timed out waiting for the user to respond.
#[error("Operation timed out waiting for the user to respond")]
Timeout,
/// An error occurred while finding the `pinentry` binary.
#[error("{0}")]
Which(#[from] which::Error),
/// An I/O error occurred while communicating with the `pinentry` binary.
#[error("{0}")]
Io(#[from] io::Error),
/// An uncommon or unexpected GPG error.
#[error("{0}")]
Gpg(#[from] GpgError),
/// The user's input doesn't decode to valid UTF-8.
#[error("{0}")]
Encoding(#[from] std::str::Utf8Error),
}
/*
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Timeout => write!(f, "Operation timed out"),
Error::Cancelled => write!(f, "Operation cancelled"),
Error::Gpg(e) => e.fmt(f),
Error::Io(e) => e.fmt(f),
Error::Encoding(e) => e.fmt(f),
}
}
}
*/
/*
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
impl From<std::str::Utf8Error> for Error {
fn from(e: std::str::Utf8Error) -> Self {
Error::Encoding(e)
}
}
*/
impl Error {
pub(crate) fn from_parts(code: u16, description: Option<String>) -> Self {
match code {
GPG_ERR_TIMEOUT => Error::Timeout,
GPG_ERR_CANCELED => Error::Cancelled,
_ => Error::Gpg(GpgError::new(code, description)),
}
}
}

455
keyfork-pinentry/src/lib.rs Normal file
View File

@ -0,0 +1,455 @@
//! `keyfork_pinentry` is a library for interacting with the pinentry binaries available on
//! various platforms.
//!
//! # Examples
//!
//! ## Request passphrase or PIN
//!
//! ```no_run
//! use keyfork_pinentry::PassphraseInput;
//! use secrecy::SecretString;
//!
//! let passphrase = if let Ok(mut input) = PassphraseInput::with_default_binary() {
//! // pinentry binary is available!
//! input
//! .with_description("Enter new passphrase for FooBar")
//! .with_prompt("Passphrase:")
//! .with_confirmation("Confirm passphrase:", "Passphrases do not match")
//! .interact()
//! } else {
//! // Fall back to some other passphrase entry method.
//! Ok(SecretString::new("a better passphrase than this".to_owned()))
//! }?;
//! # Ok::<(), keyfork_pinentry::Error>(())
//! ```
//!
//! ## Ask user for confirmation
//!
//! ```no_run
//! use keyfork_pinentry::ConfirmationDialog;
//!
//! if let Ok(mut input) = ConfirmationDialog::with_default_binary() {
//! input
//! .with_ok("Definitely!")
//! .with_not_ok("No thanks")
//! .with_cancel("Maybe later")
//! .confirm("Would you like to play a game?")?;
//! };
//! # Ok::<(), keyfork_pinentry::Error>(())
//! ```
//!
//! ## Display a message
//!
//! ```no_run
//! use keyfork_pinentry::MessageDialog;
//!
//! if let Ok(mut input) = MessageDialog::with_default_binary() {
//! input.with_ok("Got it!").show_message("This will be shown with a single button.")?;
//! };
//! # Ok::<(), keyfork_pinentry::Error>(())
//! ```
// Catch documentation errors caused by code changes.
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(missing_docs)]
pub use secrecy::{ExposeSecret, SecretString};
use std::path::PathBuf;
mod assuan;
mod error;
pub use error::{Error, GpgError};
/// Result type for the `keyfork_pinentry` crate.
pub type Result<T> = std::result::Result<T, Error>;
/// Find the expected default pinentry binary
pub fn default_binary() -> Result<PathBuf> {
which::which("pinentry-curses").map_err(Into::into)
}
fn convert_multiline(line: &str) -> String {
// convert into multiline
let mut converted_line = String::new();
let mut last_end = 0;
for (start, part) in line.match_indices(&['\n', '\r', '%']) {
converted_line.push_str(line.get(last_end..start).unwrap());
converted_line.push_str(match part {
"\n" => "%0A",
"\r" => "%0D",
"%" => "%25",
fb => panic!("expected index given to match_indices, got: {fb}"),
});
last_end = start + part.len();
}
converted_line.push_str(line.get(last_end..line.len()).unwrap());
converted_line
}
/// A dialog for requesting a passphrase from the user.
pub struct PassphraseInput<'a> {
binary: PathBuf,
required: Option<&'a str>,
title: Option<&'a str>,
description: Option<&'a str>,
error: Option<&'a str>,
prompt: Option<&'a str>,
confirmation: Option<(&'a str, &'a str)>,
ok: Option<&'a str>,
cancel: Option<&'a str>,
timeout: Option<u16>,
}
impl<'a> PassphraseInput<'a> {
/// Creates a new PassphraseInput using the binary named `keyfork_pinentry`.
///
/// Returns `Err` if `default_binary()` cannot be found in `PATH`.
pub fn with_default_binary() -> Result<Self> {
default_binary().map(Self::with_binary)
}
/// Creates a new PassphraseInput using the given path to, or name of, a `pinentry`
/// binary.
pub fn with_binary(binary: PathBuf) -> Self {
PassphraseInput {
binary,
required: None,
title: None,
description: None,
error: None,
prompt: None,
confirmation: None,
ok: None,
cancel: None,
timeout: None,
}
}
/// Prevents the user from submitting an empty passphrase.
///
/// The provided error text will be displayed if the user submits an empty passphrase.
/// The dialog will remain open until the user either submits a non-empty passphrase,
/// or selects the "Cancel" button.
pub fn required(&mut self, empty_error: &'a str) -> &mut Self {
self.required = Some(empty_error);
self
}
/// Sets the window title.
///
/// When using this feature you should take care that the window is still identifiable
/// as the pinentry.
pub fn with_title(&mut self, title: &'a str) -> &mut Self {
self.title = Some(title);
self
}
/// Sets the descriptive text to display.
pub fn with_description(&mut self, description: &'a str) -> &mut Self {
self.description = Some(description);
self
}
/// Sets the error text to display.
///
/// This is used to display an error message, for example on a second interaction if
/// the first passphrase was invalid.
pub fn with_error(&mut self, error: &'a str) -> &mut Self {
self.error = Some(error);
self
}
/// Sets the prompt to show.
///
/// When asking for a passphrase or PIN, this sets the text just before the widget for
/// passphrase entry.
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_prompt(&mut self, prompt: &'a str) -> &mut Self {
self.prompt = Some(prompt);
self
}
/// Enables confirmation prompting.
///
/// When asking for a passphrase or PIN, this sets the text just before the widget for
/// the passphrase confirmation entry.
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_confirmation(
&mut self,
confirmation_prompt: &'a str,
mismatch_error: &'a str,
) -> &mut Self {
self.confirmation = Some((confirmation_prompt, mismatch_error));
self
}
/// Sets the text for the button signalling confirmation (the "OK" button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_ok(&mut self, ok: &'a str) -> &mut Self {
self.ok = Some(ok);
self
}
/// Sets the text for the button signaling cancellation or disagreement (the "Cancel"
/// button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_cancel(&mut self, cancel: &'a str) -> &mut Self {
self.cancel = Some(cancel);
self
}
/// Sets the timeout (in seconds) before returning an error.
pub fn with_timeout(&mut self, timeout: u16) -> &mut Self {
self.timeout = Some(timeout);
self
}
/// Asks for a passphrase or PIN.
pub fn interact(&self) -> Result<SecretString> {
let mut pinentry = assuan::Connection::open(&self.binary)?;
if let Some(title) = &self.title {
pinentry.send_request("SETTITLE", Some(title))?;
}
if let Some(desc) = &self.description {
pinentry.send_request("SETDESC", Some(convert_multiline(desc).as_ref()))?;
}
if let Some(error) = &self.error {
pinentry.send_request("SETERROR", Some(error))?;
}
if let Some(prompt) = &self.prompt {
pinentry.send_request("SETPROMPT", Some(prompt))?;
}
if let Some(ok) = &self.ok {
pinentry.send_request("SETOK", Some(ok))?;
}
if let Some(cancel) = &self.cancel {
pinentry.send_request("SETCANCEL", Some(cancel))?;
}
if let Some((confirmation_prompt, mismatch_error)) = &self.confirmation {
pinentry.send_request("SETREPEAT", Some(confirmation_prompt))?;
pinentry.send_request("SETREPEATERROR", Some(mismatch_error))?;
}
if let Some(timeout) = self.timeout {
pinentry.send_request("SETTIMEOUT", Some(&format!("{}", timeout)))?;
}
loop {
match (pinentry.send_request("GETPIN", None)?, self.required) {
// If the user provides an empty passphrase, GETPIN returns no data.
(None, None) => return Ok(SecretString::new(String::new())),
(Some(passphrase), _) => return Ok(passphrase),
(_, Some(empty_error)) => {
// SETERROR is cleared by GETPIN, so we reset it on each loop.
pinentry.send_request("SETERROR", Some(empty_error))?;
}
}
}
}
}
/// A dialog for requesting a confirmation from the user.
pub struct ConfirmationDialog<'a> {
binary: PathBuf,
title: Option<&'a str>,
ok: Option<&'a str>,
cancel: Option<&'a str>,
not_ok: Option<&'a str>,
timeout: Option<u16>,
}
impl<'a> ConfirmationDialog<'a> {
/// Creates a new ConfirmationDialog using the binary named `pinentry`.
///
/// Returns `Err` if `pinentry` cannot be found in `PATH`.
pub fn with_default_binary() -> Result<Self> {
default_binary().map(Self::with_binary)
}
/// Creates a new ConfirmationDialog using the given path to, or name of, a `pinentry`
/// binary.
pub fn with_binary(binary: PathBuf) -> Self {
ConfirmationDialog {
binary,
title: None,
ok: None,
cancel: None,
not_ok: None,
timeout: None,
}
}
/// Sets the window title.
///
/// When using this feature you should take care that the window is still identifiable
/// as the pinentry.
pub fn with_title(&mut self, title: &'a str) -> &mut Self {
self.title = Some(title);
self
}
/// Sets the text for the button signalling confirmation (the "OK" button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_ok(&mut self, ok: &'a str) -> &mut Self {
self.ok = Some(ok);
self
}
/// Sets the text for the button signaling cancellation or disagreement (the "Cancel"
/// button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_cancel(&mut self, cancel: &'a str) -> &mut Self {
self.cancel = Some(cancel);
self
}
/// Enables the third non-affirmative response button (the "Not OK" button).
///
/// This can be used in case three buttons are required (to distinguish between
/// cancellation and disagreement).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_not_ok(&mut self, not_ok: &'a str) -> &mut Self {
self.not_ok = Some(not_ok);
self
}
/// Sets the timeout (in seconds) before returning an error.
pub fn with_timeout(&mut self, timeout: u16) -> &mut Self {
self.timeout = Some(timeout);
self
}
/// Asks for confirmation.
///
/// Returns:
/// - `Ok(true)` if the "OK" button is selected.
/// - `Ok(false)` if:
/// - the "Cancel" button is selected and the "Not OK" button is disabled.
/// - the "Not OK" button is enabled and selected.
/// - `Err(Error::Cancelled)` if the "Cancel" button is selected and the "Not OK"
/// button is enabled.
pub fn confirm(&self, query: &str) -> Result<bool> {
let mut pinentry = assuan::Connection::open(&self.binary)?;
pinentry.send_request("SETDESC", Some(query))?;
if let Some(ok) = &self.ok {
pinentry.send_request("SETOK", Some(ok))?;
}
if let Some(cancel) = &self.cancel {
pinentry.send_request("SETCANCEL", Some(cancel))?;
}
if let Some(not_ok) = &self.not_ok {
pinentry.send_request("SETNOTOK", Some(not_ok))?;
}
if let Some(timeout) = self.timeout {
pinentry.send_request("SETTIMEOUT", Some(&format!("{}", timeout)))?;
}
pinentry
.send_request("CONFIRM", None)
.map(|_| true)
.or_else(|e| match (&e, self.not_ok.is_some()) {
(Error::Cancelled, false) => Ok(false),
(Error::Gpg(gpg), true) if gpg.code() == error::GPG_ERR_NOT_CONFIRMED => Ok(false),
_ => Err(e),
})
}
}
/// A dialog for showing a message to the user.
pub struct MessageDialog<'a> {
binary: PathBuf,
title: Option<&'a str>,
ok: Option<&'a str>,
timeout: Option<u16>,
}
impl<'a> MessageDialog<'a> {
/// Creates a new MessageDialog using the binary named `pinentry`.
///
/// Returns `Err` if `pinentry` cannot be found in `PATH`.
pub fn with_default_binary() -> Result<Self> {
default_binary().map(Self::with_binary)
}
/// Creates a new MessageDialog using the given path to, or name of, a `pinentry`
/// binary.
pub fn with_binary(binary: PathBuf) -> Self {
MessageDialog {
binary,
title: None,
ok: None,
timeout: None,
}
}
/// Sets the window title.
///
/// When using this feature you should take care that the window is still identifiable
/// as the pinentry.
pub fn with_title(&mut self, title: &'a str) -> &mut Self {
self.title = Some(title);
self
}
/// Sets the text for the button signalling confirmation (the "OK" button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_ok(&mut self, ok: &'a str) -> &mut Self {
self.ok = Some(ok);
self
}
/// Sets the timeout (in seconds) before returning an error.
pub fn with_timeout(&mut self, timeout: u16) -> &mut Self {
self.timeout = Some(timeout);
self
}
/// Shows a message.
pub fn show_message(&self, message: &str) -> Result<()> {
let mut pinentry = assuan::Connection::open(&self.binary)?;
pinentry.send_request("SETDESC", Some(message))?;
if let Some(ok) = &self.ok {
pinentry.send_request("SETOK", Some(ok))?;
}
if let Some(timeout) = self.timeout {
pinentry.send_request("SETTIMEOUT", Some(&format!("{}", timeout)))?;
}
pinentry.send_request("MESSAGE", None).map(|_| ())
}
}

View File

@ -6,14 +6,21 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["openpgp"]
openpgp = ["sequoia-openpgp"]
default = ["openpgp", "openpgp-card"]
openpgp = ["sequoia-openpgp", "prompt"]
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend"]
prompt = ["keyfork-pinentry"]
[dependencies]
anyhow = "1.0.75"
bincode = "1.3.3"
card-backend = { version = "0.2.0", optional = true }
card-backend-pcsc = { version = "0.5.0", optional = true }
keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" }
keyfork-pinentry = { version = "0.5.0", path = "../keyfork-pinentry", optional = true }
openpgp-card-sequoia = { version = "0.2.0", optional = true }
sequoia-openpgp = { version = "1.16.1", optional = true }
serde = "1.0.188"
sharks = "0.5.0"
smex = { version = "0.1.0", path = "../smex" }
thiserror = "1.0.50"

View File

@ -6,19 +6,23 @@ use std::{
str::FromStr,
};
use keyfork_shard::openpgp::{combine, discover_certs, parse_messages, openpgp::Cert};
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(threshold: &str, key_discovery: &str) -> Result<(u8, Vec<Cert>)> {
fn validate<'a>(
threshold: &str,
key_discovery: impl Into<Option<&'a str>>,
) -> Result<(u8, Vec<Cert>)> {
let threshold = u8::from_str(threshold)?;
let key_discovery = PathBuf::from(key_discovery);
// Verify path exists
std::fs::metadata(&key_discovery)?;
let key_discovery = key_discovery.into().map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
// Load certs from path
let certs = discover_certs(key_discovery)?;
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((threshold, certs))
}
@ -28,8 +32,9 @@ fn run() -> Result<()> {
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (threshold, cert_list) = match args.as_slice() {
[threshold, key_discovery] => validate(threshold, key_discovery)?,
_ => panic!("Usage: {program_name} threshold key_discovery"),
[threshold, key_discovery] => validate(threshold, key_discovery.as_str())?,
[threshold] => validate(threshold, None)?,
_ => panic!("Usage: {program_name} threshold [key_discovery]"),
};
let mut encrypted_messages = parse_messages(stdin())?;
@ -53,6 +58,11 @@ fn main() -> ExitCode {
let result = run();
if let Err(e) = result {
eprintln!("Error: {e}");
let mut source = e.source();
while let Some(new_error) = source.take() {
eprintln!("Source: {new_error}");
source = new_error.source();
}
return ExitCode::FAILURE;
}
ExitCode::SUCCESS

View File

@ -1,2 +1,5 @@
#[cfg(feature = "openpgp")]
pub mod openpgp;
#[cfg(feature = "prompt")]
mod prompt_manager;

View File

@ -13,14 +13,17 @@ use openpgp::{
armor::{Kind, Writer},
cert::{Cert, CertParser, ValidCert},
packet::{Packet, Tag, UserID, PKESK, SEIP},
parse::{stream::DecryptorBuilder, Parse},
parse::{
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
Parse,
},
policy::{NullPolicy, Policy, StandardPolicy},
serialize::{
stream::{ArbitraryWriter, Encryptor, LiteralWriter, Message, Recipient, Signer},
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
Marshal,
},
types::KeyFlags,
KeyID, PacketPile,
Fingerprint, KeyID, PacketPile,
};
pub use sequoia_openpgp as openpgp;
use sharks::{Share, Sharks};
@ -28,20 +31,46 @@ use sharks::{Share, Sharks};
mod keyring;
use keyring::Keyring;
// TODO: better error handling
mod smartcard;
use smartcard::SmartcardManager;
#[derive(Debug, Clone)]
pub struct WrappedError(String);
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Error with creating Share: {0}")]
Share(String),
impl std::fmt::Display for WrappedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
#[error("Error combining shares: {0}")]
CombineShares(String),
#[error("Derived secret hash {0} != expected {1}")]
InvalidSecret(Fingerprint, Fingerprint),
#[error("OpenPGP error: {0}")]
Sequoia(#[source] anyhow::Error),
#[error("OpenPGP IO error: {0}")]
SequoiaIo(#[source] std::io::Error),
#[error("Keyring error: {0}")]
Keyring(#[from] keyring::Error),
#[error("Smartcard error: {0}")]
Smartcard(#[from] smartcard::Error),
#[error("IO error: {0}")]
Io(#[source] std::io::Error),
#[error("Derivation path: {0}")]
DerivationPath(#[from] keyfork_derive_openpgp::derive_util::path::Error),
#[error("Derivation request: {0}")]
DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError),
#[error("Keyfork OpenPGP: {0}")]
KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error),
}
impl std::error::Error for WrappedError {}
pub type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Clone)]
pub struct EncryptedMessage {
@ -57,29 +86,38 @@ impl EncryptedMessage {
}
}
pub fn decrypt_with(&self, policy: &'_ dyn Policy, keyring: &mut Keyring) -> Result<Vec<u8>> {
pub fn decrypt_with<H>(&self, policy: &'_ dyn Policy, decryptor: H) -> Result<Vec<u8>>
where
H: VerificationHelper + DecryptionHelper,
{
let mut packets = vec![];
for pkesk in &self.pkesks {
let mut packet = vec![];
pkesk.serialize(&mut packet)?;
pkesk.serialize(&mut packet).map_err(Error::Sequoia)?;
let message = Message::new(&mut packets);
let mut message = ArbitraryWriter::new(message, Tag::PKESK)?;
message.write_all(&packet)?;
message.finalize()?;
let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
}
let mut packet = vec![];
self.message.serialize(&mut packet)?;
self.message
.serialize(&mut packet)
.map_err(Error::Sequoia)?;
let message = Message::new(&mut packets);
let mut message = ArbitraryWriter::new(message, Tag::SEIP)?;
message.write_all(&packet)?;
message.finalize()?;
let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
let mut decryptor =
DecryptorBuilder::from_bytes(&packets)?.with_policy(policy, None, keyring)?;
let mut decryptor = DecryptorBuilder::from_bytes(&packets)
.map_err(Error::Sequoia)?
.with_policy(policy, None, decryptor)
.map_err(Error::Sequoia)?;
let mut content = vec![];
decryptor.read_to_end(&mut content)?;
decryptor
.read_to_end(&mut content)
.map_err(Error::SequoiaIo)?;
Ok(content)
}
}
@ -89,18 +127,19 @@ pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
if path.is_file() {
let mut vec = vec![];
for cert in CertParser::from_file(path)? {
vec.push(cert?);
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
vec.push(cert.map_err(Error::Sequoia)?);
}
Ok(vec)
} else {
let mut vec = vec![];
for entry in path
.read_dir()?
.read_dir()
.map_err(Error::Io)?
.filter_map(Result::ok)
.filter(|p| p.path().is_file())
{
vec.push(Cert::from_file(entry.path())?);
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
}
Ok(vec)
}
@ -110,7 +149,10 @@ pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<Encryp
let mut pkesks = Vec::new();
let mut encrypted_messages = VecDeque::new();
for packet in PacketPile::from_reader(reader)?.into_children() {
for packet in PacketPile::from_reader(reader)
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => {
@ -167,17 +209,29 @@ pub fn combine(
// We don't want to invalidate someone's keys just because the old sig expired.
let policy = NullPolicy::new();
let mut keyring = Keyring::new(certs);
let content = metadata.decrypt_with(&policy, &mut keyring)?;
let mut keyring = Keyring::new(certs)?;
let mut manager = SmartcardManager::new()?;
let content = if keyring.is_empty() {
// NOTE: Any card plugged in that can't decrypt, will raise issues.
// This should not be used on a system where OpenPGP cards are available that shouldn't be
// used, due to the nature of how wildcard decryption works.
manager.load_any_card()?;
metadata.decrypt_with(&policy, &mut manager)?
} else {
metadata.decrypt_with(&policy, &mut keyring)?
};
let mut cert_parser = CertParser::from_bytes(&content)?;
let mut cert_parser = CertParser::from_bytes(&content).map_err(Error::Sequoia)?;
let root_cert = match cert_parser.next() {
Some(Ok(c)) => c,
Some(Err(e)) => panic!("Could not find root (first) certificate: {e}"),
None => panic!("No certs found in cert parser"),
};
let certs = cert_parser.collect::<openpgp::Result<Vec<_>>>()?;
keyring.set_root_cert(root_cert);
let certs = cert_parser
.collect::<openpgp::Result<Vec<_>>>()
.map_err(Error::Sequoia)?;
keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert);
let mut messages: HashMap<KeyID, EncryptedMessage> =
HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages));
let mut decrypted_messages: HashMap<KeyID, Vec<u8>> = HashMap::new();
@ -185,12 +239,14 @@ pub fn combine(
// NOTE: This is ONLY stable because we control the generation of PKESK packets and
// encode the policy to ourselves.
for valid_cert in certs.iter().map(|cert| cert.with_policy(&policy, None)) {
let valid_cert = valid_cert?;
let valid_cert = valid_cert.map_err(Error::Sequoia)?;
// get keys from keyring for cert
let Some(secret_cert) = keyring.get_cert_for_primary_keyid(&valid_cert.keyid()) else {
continue;
};
let secret_cert = secret_cert.with_policy(&policy, None)?;
let secret_cert = secret_cert
.with_policy(&policy, None)
.map_err(Error::Sequoia)?;
let keys = get_decryption_keys(&secret_cert).collect::<Vec<_>>();
if !keys.is_empty() {
if let Some(message) = messages.get_mut(&valid_cert.keyid()) {
@ -198,20 +254,10 @@ pub fn combine(
pkesk.set_recipient(key.keyid());
}
// we have a pkesk, decrypt via keyring
let result = message.decrypt_with(&policy, &mut keyring);
match result {
Ok(message) => {
decrypted_messages.insert(valid_cert.keyid(), message);
}
Err(e) => {
eprintln!(
"Could not decrypt with fingerprint {}: {}",
valid_cert.keyid(),
e
);
// do nothing, key will be retained
}
}
decrypted_messages.insert(
valid_cert.keyid(),
message.decrypt_with(&policy, &mut keyring)?,
);
}
}
}
@ -221,18 +267,53 @@ pub fn combine(
let left_from_threshold = threshold as usize - decrypted_messages.len();
if left_from_threshold > 0 {
eprintln!("remaining keys: {left_from_threshold}, prompting yubikeys");
}
for _ in 0..left_from_threshold {
todo!("prompt for Yubikeys")
let mut remaining_usable_certs = certs
.iter()
.filter(|cert| messages.contains_key(&cert.keyid()))
.collect::<Vec<_>>();
while threshold as usize - decrypted_messages.len() > 0 {
remaining_usable_certs.retain(|cert| messages.contains_key(&cert.keyid()));
let mut key_by_fingerprints = HashMap::new();
let mut total_fingerprints = vec![];
for valid_cert in remaining_usable_certs
.iter()
.map(|cert| cert.with_policy(&policy, None))
{
let valid_cert = valid_cert.map_err(Error::Sequoia)?;
let fp = valid_cert
.keys()
.for_storage_encryption()
.map(|k| k.fingerprint())
.collect::<Vec<_>>();
for fp in &fp {
key_by_fingerprints.insert(fp.clone(), valid_cert.keyid());
}
total_fingerprints.extend(fp.iter().cloned());
}
// Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message
if let Some(fp) = manager.load_any_fingerprint(total_fingerprints)? {
// soundness: `key_by_fingerprints` is extended by the same fps that are then
// inserted into `total_fingerprints`
let cert_keyid = key_by_fingerprints.get(&fp).unwrap().clone();
let message = messages.remove(&cert_keyid);
if let Some(message) = message {
let message = message.decrypt_with(&policy, &mut manager)?;
decrypted_messages.insert(cert_keyid, message);
}
}
}
}
let shares = decrypted_messages
.values()
.map(|message| Share::try_from(message.as_slice()))
.collect::<Result<Vec<_>, &str>>()
.map_err(|e| WrappedError(e.to_string()))?;
let secret = Sharks(threshold).recover(&shares)?;
.map_err(|e| Error::Share(e.to_string()))?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| Error::CombineShares(e.to_string()))?;
let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new(
@ -247,19 +328,18 @@ pub fn combine(
)?;
// NOTE: Signatures on certs will be different. Compare fingerprints instead.
if Some(derived_cert.fingerprint()) != keyring.root_cert().map(Cert::fingerprint) {
return Err(WrappedError(format!(
"Derived {} != expected {}",
derived_cert.fingerprint(),
keyring
.root_cert()
.expect("cert was previously set")
.fingerprint()
))
.into());
let derived_fp = derived_cert.fingerprint();
let expected_fp = keyring
.root_cert()
.expect("cert was previously set")
.fingerprint();
if derived_fp != expected_fp {
return Err(Error::InvalidSecret(derived_fp, expected_fp));
}
output.write_all(smex::encode(&secret).as_bytes())?;
output
.write_all(smex::encode(&secret).as_bytes())
.map_err(Error::Io)?;
Ok(())
}
@ -279,55 +359,63 @@ pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write)
)?;
let signing_key = derived_cert
.primary_key()
.parts_into_secret()?
.parts_into_secret()
.map_err(Error::Sequoia)?
.key()
.clone()
.into_keypair()?;
.into_keypair()
.map_err(Error::Sequoia)?;
let sharks = Sharks(threshold);
let dealer = sharks.dealer(secret);
let shares = dealer.map(|s| Vec::from(&s)).collect::<Vec<_>>();
let policy = StandardPolicy::new();
let mut writer = Writer::new(output, Kind::Message)?;
let mut writer = Writer::new(output, Kind::Message).map_err(Error::SequoiaIo)?;
let mut total_recipients = vec![];
let mut messages = vec![];
for (share, cert) in shares.iter().zip(certs) {
total_recipients.push(cert.clone());
let valid_cert = cert.with_policy(&policy, None)?;
let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?;
let encryption_keys = get_encryption_keys(&valid_cert).collect::<Vec<_>>();
let mut message_output = vec![];
let message = Message::new(&mut message_output);
let message = Encryptor::for_recipients(
let message = Encryptor2::for_recipients(
message,
encryption_keys
.iter()
.map(|k| Recipient::new(KeyID::wildcard(), k.key())),
)
.build()?;
let message = Signer::new(message, signing_key.clone()).build()?;
let mut message = LiteralWriter::new(message).build()?;
message.write_all(share)?;
message.finalize()?;
.build()
.map_err(Error::Sequoia)?;
let message = Signer::new(message, signing_key.clone())
.build()
.map_err(Error::Sequoia)?;
let mut message = LiteralWriter::new(message)
.build()
.map_err(Error::Sequoia)?;
message.write_all(share).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
messages.push(message_output);
}
let mut pp = vec![];
// store derived cert to verify provided shares
derived_cert.serialize(&mut pp)?;
derived_cert.serialize(&mut pp).map_err(Error::Sequoia)?;
for recipient in &total_recipients {
recipient.serialize(&mut pp)?;
recipient.serialize(&mut pp).map_err(Error::Sequoia)?;
}
// verify packet pile
for (packet_cert, cert) in openpgp::cert::CertParser::from_bytes(&pp)?
for (packet_cert, cert) in openpgp::cert::CertParser::from_bytes(&pp)
.map_err(Error::Sequoia)?
.skip(1)
.zip(total_recipients.iter())
{
if packet_cert? != *cert {
if packet_cert.map_err(Error::Sequoia)? != *cert {
panic!(
"packet pile could not recreate cert: {}",
cert.fingerprint()
@ -338,7 +426,8 @@ pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write)
let valid_certs = total_recipients
.iter()
.map(|c| c.with_policy(&policy, None))
.collect::<openpgp::Result<Vec<_>>>()?;
.collect::<openpgp::Result<Vec<_>>>()
.map_err(Error::Sequoia)?;
let total_recipients = valid_certs.iter().flat_map(|vc| {
get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key()))
@ -347,17 +436,23 @@ pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write)
// metadata
let mut message_output = vec![];
let message = Message::new(&mut message_output);
let message = Encryptor::for_recipients(message, total_recipients).build()?;
let mut message = LiteralWriter::new(message).build()?;
message.write_all(&pp)?;
message.finalize()?;
writer.write_all(&message_output)?;
let message = Encryptor2::for_recipients(message, total_recipients)
.build()
.map_err(Error::Sequoia)?;
let mut message = LiteralWriter::new(message)
.build()
.map_err(Error::Sequoia)?;
message.write_all(&pp).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
writer
.write_all(&message_output)
.map_err(Error::SequoiaIo)?;
for message in messages {
writer.write_all(&message)?;
writer.write_all(&message).map_err(Error::SequoiaIo)?;
}
writer.finalize()?;
writer.finalize().map_err(Error::SequoiaIo)?;
Ok(())
}

View File

@ -1,43 +1,46 @@
use keyfork_pinentry::ExposeSecret;
use super::openpgp::{
self,
cert::Cert,
packet::{PKESK, SKESK},
parse::stream::{DecryptionHelper, MessageLayer, MessageStructure, VerificationHelper},
policy::NullPolicy,
KeyHandle, KeyID,
};
#[derive(Clone, Debug)]
pub enum KeyringFailure {
use crate::prompt_manager::{PinentryError, PromptManager};
use anyhow::Context;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Secret key was not found")]
SecretKeyNotFound,
#[allow(dead_code)]
SmartcardDecrypt,
#[error("Prompt failed: {0}")]
Prompt(#[from] PinentryError),
}
impl std::fmt::Display for KeyringFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KeyringFailure::SecretKeyNotFound => f.write_str("Secret key was not found"),
KeyringFailure::SmartcardDecrypt => {
f.write_str("Smartcard could not decrypt any PKESKs")
}
}
}
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
impl std::error::Error for KeyringFailure {}
#[derive(Clone, Debug)]
pub struct Keyring {
full_certs: Vec<Cert>,
root: Option<Cert>,
pm: PromptManager,
}
impl Keyring {
pub fn new(certs: impl AsRef<[Cert]>) -> Self {
Self {
pub fn new(certs: impl AsRef<[Cert]>) -> Result<Self> {
Ok(Self {
full_certs: certs.as_ref().to_vec(),
root: Default::default(),
}
pm: PromptManager::new("keyfork-shard", None)?,
})
}
pub fn is_empty(&self) -> bool {
self.full_certs.is_empty()
}
// Sets the root cert, returning the old cert
@ -88,8 +91,9 @@ impl VerificationHelper for &mut Keyring {
MessageLayer::SignatureGroup { results } => {
for result in results {
if let Err(e) = result {
// FIXME: anyhow leak
return Err(anyhow::anyhow!(e.to_string()));
// FIXME: anyhow leak: VerificationError impl std::error::Error
// return Err(e.context("Invalid signature"));
return Err(anyhow::anyhow!("Invalid signature: {e}"));
}
}
}
@ -110,14 +114,46 @@ impl DecryptionHelper for &mut Keyring {
where
D: FnMut(openpgp::types::SymmetricAlgorithm, &openpgp::crypto::SessionKey) -> bool,
{
// optimized route: use all locally stored certs
let null = NullPolicy::new();
// unoptimized route: use all locally stored certs
for pkesk in pkesks {
for cert in self.get_certs_for_pkesk(pkesk) {
for key in cert.keys().secret() {
#[allow(deprecated, clippy::map_flatten)]
let name = cert
.userids()
.next()
.map(|userid| userid.userid().name().transpose())
.flatten()
.transpose()
.ok()
.flatten();
for key in cert
.keys()
.with_policy(&null, None)
.for_storage_encryption()
.secret()
{
let secret_key = key.key().clone();
// NOTE: Returns an error if using an encrypted secret key.
// TODO: support skipping or validating encrypted secret keys.
let mut keypair = secret_key.into_keypair()?;
let mut keypair = if secret_key.has_unencrypted_secret() {
secret_key
.into_keypair()
.context("Has unencrypted secret")?
} else {
let message = if let Some(name) = name.as_ref() {
format!("Decryption key for: {} ({name})", secret_key.keyid())
} else {
format!("Decryption key for: {}", secret_key.keyid())
};
let passphrase = self
.pm
.prompt_passphrase("Decryption passphrase", message)
.context("Decryption passphrase")?;
secret_key
.decrypt_secret(&passphrase.expose_secret().as_str().into())
.context("has_unencrypted_secret is false, could not decrypt secret")?
.into_keypair()
.context("just-decrypted key")?
};
if pkesk
.decrypt(&mut keypair, sym_algo)
.map(|(algo, sk)| decrypt(algo, &sk))
@ -129,8 +165,6 @@ impl DecryptionHelper for &mut Keyring {
}
}
// smartcard route: plug in smartcard, attempt decrypt, fail and bail
Err(KeyringFailure::SecretKeyNotFound.into())
Err(Error::SecretKeyNotFound.into())
}
}

View File

@ -0,0 +1,261 @@
use std::collections::HashSet;
use keyfork_pinentry::ExposeSecret;
use super::openpgp::{
self,
cert::Cert,
packet::{PKESK, SKESK},
parse::stream::{DecryptionHelper, MessageLayer, MessageStructure, VerificationHelper},
Fingerprint,
};
use crate::prompt_manager::{PinentryError, PromptManager};
use anyhow::Context;
use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::Error as SequoiaCardError, Card};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("No smart card backend was stored")]
SmartCardNotFound,
#[error("Selected smart card has no decryption key")]
SmartCardHasNoDecrypt,
#[error("Smart card backend error: {0}")]
SmartCardBackend(#[from] card_backend::SmartcardError),
#[error("Smartcard password status unavailable: {0}")]
PwStatusBytes(SequoiaCardError),
#[error("Could not open smart card")]
OpenSmartCard(SequoiaCardError),
#[error("Could not initialize transaction")]
Transaction(SequoiaCardError),
#[error("Could not load fingerprints")]
Fingerprints(SequoiaCardError),
#[error("Invalid PIN entered too many times")]
InvalidPIN,
#[error("Prompt failed: {0}")]
Prompt(#[from] PinentryError),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
fn format_name(input: impl AsRef<str>) -> String {
let mut n = input
.as_ref()
.split("<<")
.take(2)
.map(|s| s.replace('>', " "))
.collect::<Vec<_>>();
n.reverse();
n.join(" ")
}
pub struct SmartcardManager {
current_card: Option<Card<Open>>,
root: Option<Cert>,
pm: PromptManager,
}
impl SmartcardManager {
pub fn new() -> Result<Self> {
Ok(Self {
current_card: None,
root: None,
pm: PromptManager::new("keyfork-shard", None)?,
})
}
// Sets the root cert, returning the old cert
pub fn set_root_cert(&mut self, cert: impl Into<Option<Cert>>) -> Option<Cert> {
let mut cert = cert.into();
std::mem::swap(&mut self.root, &mut cert);
cert
}
/// Load any backend.
pub fn load_any_card(&mut self) -> Result<Fingerprint> {
PcscBackend::cards(None)?
.next()
.transpose()?
.ok_or(Error::SmartCardNotFound)
.and_then(|backend| {
let mut card = Card::<Open>::new(backend).map_err(Error::OpenSmartCard)?;
let transaction = card.transaction().map_err(Error::Transaction)?;
let fingerprint = transaction
.fingerprints()
.map_err(Error::Fingerprints)?
.decryption()
.map(|fp| Fingerprint::from_bytes(fp.as_bytes()))
.ok_or(Error::SmartCardHasNoDecrypt)?;
drop(transaction);
self.current_card.replace(card);
Ok(fingerprint)
})
}
/// Load a backend if any [`Fingerprint`] has been matched by a currently active card.
///
/// NOTE: Only implemented for decryption keys.
pub fn load_any_fingerprint(
&mut self,
fingerprints: impl IntoIterator<Item = Fingerprint>,
) -> Result<Option<Fingerprint>> {
// NOTE: This can't be HashSet::from_iter() because from_iter() requires a passed-in state
// I do not want to provide.
let mut requested_fingerprints = HashSet::new();
requested_fingerprints.extend(fingerprints);
let mut had_any_backend = false;
while !had_any_backend {
// Load all backends, confirm if any have any fingerprints
for backend in PcscBackend::cards(None)? {
had_any_backend = true;
let backend = backend?;
let mut card = Card::<Open>::new(backend).map_err(Error::OpenSmartCard)?;
let transaction = card.transaction().map_err(Error::Transaction)?;
let mut fingerprint = None;
if let Some(fp) = transaction
.fingerprints()
.map_err(Error::Fingerprints)?
.decryption()
.map(|fp| Fingerprint::from_bytes(fp.as_bytes()))
{
if requested_fingerprints.contains(&fp) {
fingerprint.replace(fp);
}
}
drop(transaction);
if fingerprint.is_some() {
self.current_card.replace(card);
return Ok(fingerprint);
}
}
#[rustfmt::skip]
self.pm.prompt_message("Please plug in a smart card and press enter")?;
}
Ok(None)
}
}
impl VerificationHelper for &mut SmartcardManager {
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
Ok(ids
.iter()
.flat_map(|kh| self.root.as_ref().filter(|cert| cert.key_handle() == *kh))
.cloned()
.collect())
}
fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> {
for layer in structure.into_iter() {
#[allow(unused_variables)]
match layer {
MessageLayer::Compression { algo } => {}
MessageLayer::Encryption {
sym_algo,
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()));
}
}
}
}
}
Ok(())
}
}
impl DecryptionHelper for &mut SmartcardManager {
fn decrypt<D>(
&mut self,
pkesks: &[PKESK],
_skesks: &[SKESK],
sym_algo: Option<openpgp::types::SymmetricAlgorithm>,
mut decrypt: D,
) -> openpgp::Result<Option<Fingerprint>>
where
D: FnMut(openpgp::types::SymmetricAlgorithm, &openpgp::crypto::SessionKey) -> bool,
{
let mut card = self.current_card.take();
let Some(card) = card.as_mut() else {
return Err(Error::SmartCardNotFound.into());
};
let mut transaction = card
.transaction()
.context("Could not initialize transaction")?;
let fp = transaction
.fingerprints()
.context("Could not load fingerprints")?
.decryption()
.map(|fp| Fingerprint::from_bytes(fp.as_bytes()))
.ok_or(Error::SmartCardHasNoDecrypt)?;
let cardholder_name = format_name(
transaction
.cardholder_name()
.context("Could not load (optionally empty) cardholder name")?,
);
let card_id = transaction
.application_identifier()
.context("Could not load application identifier")?
.ident();
let pw_status = transaction
.pw_status_bytes()
.map_err(Error::PwStatusBytes)?;
let mut pin = None;
for _ in 0..pw_status.err_count_pw1() {
transaction.reload_ard()?;
let attempts = transaction
.pw_status_bytes()
.map_err(Error::PwStatusBytes)?
.err_count_pw1();
let rpea = "Remaining PIN entry attempts";
let message = if cardholder_name.is_empty() {
format!("Unlock card {card_id}\n\n{rpea}: {attempts}")
} else {
format!("Unlock card {card_id} ({cardholder_name})\n\n{rpea}: {attempts}")
};
let temp_pin = self.pm.prompt_passphrase("Smartcard User PIN", message)?;
if transaction
.verify_user_pin(temp_pin.expose_secret().as_str().trim())
.is_ok()
{
pin.replace(temp_pin);
break;
}
}
let pin = pin.ok_or(Error::InvalidPIN)?;
let mut user = transaction
.to_user_card(pin.expose_secret().as_str().trim())
.context("Could not load user smartcard from PIN")?;
let mut decryptor = user
.decryptor(&|| eprintln!("Touch confirmation needed for decryption"))
.context("Could not decrypt using smartcard")?;
for pkesk in pkesks {
if pkesk
.decrypt(&mut decryptor, sym_algo)
.map(|(algo, sk)| decrypt(algo, &sk))
.unwrap_or(false)
{
return Ok(Some(fp));
}
}
Err(Error::SmartCardNotFound.into())
}
}

View File

@ -0,0 +1,74 @@
use std::path::PathBuf;
use keyfork_pinentry::{
self, default_binary, ConfirmationDialog, MessageDialog, PassphraseInput, SecretString,
};
#[derive(thiserror::Error, Debug)]
pub enum PinentryError {
#[error("No pinentry binary found")]
NoPinentryFound,
#[error("{0}")]
Internal(#[from] keyfork_pinentry::Error),
}
pub type Result<T, E = PinentryError> = std::result::Result<T, E>;
/// Display message dialogues, confirmation prompts, and passphrase inputs with keyfork-pinentry.
pub struct PromptManager {
program_title: String,
pinentry_binary: PathBuf,
}
impl PromptManager {
pub fn new(
program_title: impl Into<String>,
pinentry_binary: impl Into<Option<PathBuf>>,
) -> Result<Self> {
let path = match pinentry_binary.into() {
Some(p) => p,
None => default_binary()?,
};
std::fs::metadata(&path).map_err(|_| PinentryError::NoPinentryFound)?;
Ok(Self {
program_title: program_title.into(),
pinentry_binary: path,
})
}
#[allow(dead_code)]
pub fn prompt_confirmation(&self, prompt: impl AsRef<str>) -> Result<bool> {
ConfirmationDialog::with_binary(self.pinentry_binary.clone())
.with_title(&self.program_title)
.confirm(prompt.as_ref())
.map_err(|e| e.into())
}
pub fn prompt_message(&self, prompt: impl AsRef<str>) -> Result<()> {
MessageDialog::with_binary(self.pinentry_binary.clone())
.with_title(&self.program_title)
.show_message(prompt.as_ref())
.map_err(|e| e.into())
}
pub fn prompt_passphrase(
&self,
prompt: impl AsRef<str>,
description: impl Into<Option<String>>,
) -> Result<SecretString> {
match description.into() {
Some(desc) => PassphraseInput::with_binary(self.pinentry_binary.clone())
.with_title(&self.program_title)
.with_prompt(prompt.as_ref())
.with_description(&desc)
.interact()
.map_err(|e| e.into()),
None => PassphraseInput::with_binary(self.pinentry_binary.clone())
.with_title(&self.program_title)
.with_prompt(prompt.as_ref())
.interact()
.map_err(|e| e.into()),
}
}
}

View File

@ -219,37 +219,67 @@ pub fn test_data() -> Result<HashMap<String, Vec<TestData>>, Box<dyn std::error:
Test {
chain: "m/0'",
fingerprint: hex::decode("ddebc675")?,
chain_code: hex::decode("8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69")?,
private_key: hex::decode("68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3")?,
public_key: hex::decode("008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c")?,
chain_code: hex::decode(
"8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69",
)?,
private_key: hex::decode(
"68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3",
)?,
public_key: hex::decode(
"008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c",
)?,
},
Test {
chain: "m/0'/1'",
fingerprint: hex::decode("13dab143")?,
chain_code: hex::decode("a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14")?,
private_key: hex::decode("b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2")?,
public_key: hex::decode("001932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187")?,
chain_code: hex::decode(
"a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14",
)?,
private_key: hex::decode(
"b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2",
)?,
public_key: hex::decode(
"001932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187",
)?,
},
Test {
chain: "m/0'/1'/2'",
fingerprint: hex::decode("ebe4cb29")?,
chain_code: hex::decode("2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c")?,
private_key: hex::decode("92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9")?,
public_key: hex::decode("00ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1")?,
chain_code: hex::decode(
"2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c",
)?,
private_key: hex::decode(
"92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9",
)?,
public_key: hex::decode(
"00ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1",
)?,
},
Test {
chain: "m/0'/1'/2'/2'",
fingerprint: hex::decode("316ec1c6")?,
chain_code: hex::decode("8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc")?,
private_key: hex::decode("30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662")?,
public_key: hex::decode("008abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c")?,
chain_code: hex::decode(
"8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc",
)?,
private_key: hex::decode(
"30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662",
)?,
public_key: hex::decode(
"008abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c",
)?,
},
Test {
chain: "m/0'/1'/2'/2'/1000000000'",
fingerprint: hex::decode("d6322ccd")?,
chain_code: hex::decode("68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230")?,
private_key: hex::decode("8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793")?,
public_key: hex::decode("003c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a")?,
chain_code: hex::decode(
"68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230",
)?,
private_key: hex::decode(
"8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793",
)?,
public_key: hex::decode(
"003c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a",
)?,
},
],
},
@ -259,44 +289,80 @@ pub fn test_data() -> Result<HashMap<String, Vec<TestData>>, Box<dyn std::error:
Test {
chain: "m",
fingerprint: hex::decode("00000000")?,
chain_code: hex::decode("ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b")?,
private_key: hex::decode("171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012")?,
public_key: hex::decode("008fe9693f8fa62a4305a140b9764c5ee01e455963744fe18204b4fb948249308a")?,
chain_code: hex::decode(
"ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b",
)?,
private_key: hex::decode(
"171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012",
)?,
public_key: hex::decode(
"008fe9693f8fa62a4305a140b9764c5ee01e455963744fe18204b4fb948249308a",
)?,
},
Test {
chain: "m/0'",
fingerprint: hex::decode("31981b50")?,
chain_code: hex::decode("0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d")?,
private_key: hex::decode("1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635")?,
public_key: hex::decode("0086fab68dcb57aa196c77c5f264f215a112c22a912c10d123b0d03c3c28ef1037")?,
chain_code: hex::decode(
"0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d",
)?,
private_key: hex::decode(
"1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635",
)?,
public_key: hex::decode(
"0086fab68dcb57aa196c77c5f264f215a112c22a912c10d123b0d03c3c28ef1037",
)?,
},
Test {
chain: "m/0'/2147483647'",
fingerprint: hex::decode("1e9411b1")?,
chain_code: hex::decode("138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f")?,
private_key: hex::decode("ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4")?,
public_key: hex::decode("005ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d")?,
chain_code: hex::decode(
"138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f",
)?,
private_key: hex::decode(
"ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4",
)?,
public_key: hex::decode(
"005ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d",
)?,
},
Test {
chain: "m/0'/2147483647'/1'",
fingerprint: hex::decode("fcadf38c")?,
chain_code: hex::decode("73bd9fff1cfbde33a1b846c27085f711c0fe2d66fd32e139d3ebc28e5a4a6b90")?,
private_key: hex::decode("3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c")?,
public_key: hex::decode("002e66aa57069c86cc18249aecf5cb5a9cebbfd6fadeab056254763874a9352b45")?,
chain_code: hex::decode(
"73bd9fff1cfbde33a1b846c27085f711c0fe2d66fd32e139d3ebc28e5a4a6b90",
)?,
private_key: hex::decode(
"3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c",
)?,
public_key: hex::decode(
"002e66aa57069c86cc18249aecf5cb5a9cebbfd6fadeab056254763874a9352b45",
)?,
},
Test {
chain: "m/0'/2147483647'/1'/2147483646'",
fingerprint: hex::decode("aca70953")?,
chain_code: hex::decode("0902fe8a29f9140480a00ef244bd183e8a13288e4412d8389d140aac1794825a")?,
private_key: hex::decode("5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72")?,
public_key: hex::decode("00e33c0f7d81d843c572275f287498e8d408654fdf0d1e065b84e2e6f157aab09b")?,
chain_code: hex::decode(
"0902fe8a29f9140480a00ef244bd183e8a13288e4412d8389d140aac1794825a",
)?,
private_key: hex::decode(
"5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72",
)?,
public_key: hex::decode(
"00e33c0f7d81d843c572275f287498e8d408654fdf0d1e065b84e2e6f157aab09b",
)?,
},
Test {
chain: "m/0'/2147483647'/1'/2147483646'/2'",
fingerprint: hex::decode("422c654b")?,
chain_code: hex::decode("5d70af781f3a37b829f0d060924d5e960bdc02e85423494afc0b1a41bbe196d4")?,
private_key: hex::decode("551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d")?,
public_key: hex::decode("0047150c75db263559a70d5778bf36abbab30fb061ad69f69ece61a72b0cfa4fc0")?,
chain_code: hex::decode(
"5d70af781f3a37b829f0d060924d5e960bdc02e85423494afc0b1a41bbe196d4",
)?,
private_key: hex::decode(
"551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d",
)?,
public_key: hex::decode(
"0047150c75db263559a70d5778bf36abbab30fb061ad69f69ece61a72b0cfa4fc0",
)?,
},
],
},

View File

@ -8,16 +8,20 @@ Combine `threshold` shares into a previously [`split`] secret.
* `threshold`: Minimum number of operators present to recover the secret, as
previously configured when creating the secret
* `key_discovery`: Either a file or a directory containing OpenPGP keys.
If a file, load all keys from the file.
If a directory, for every file in the directory (non-recursively), load
keys from the file.
* `key_discovery`: A directory containing OpenPGP keys.
If the amount of keys found is less than `threshold`, an OpenPGP Card
fallback will be used to decrypt the rest of the messages.
## Pinentry
The terminal may be overridden if the default pinentry command is
`pinentry-curses`, but this will affect neither input nor output.` Pinentry is
used if an OpenPGP key file has an encrypted secret key or to prompt for the
PIN for an OpenPGP smart card.
## Input
OpenPGP Messages from [`split`].
OpenPGP messages from [`split`].
## Output

View File

@ -1,5 +1,5 @@
use super::Keyfork;
use clap::{Parser, Subcommand, ValueEnum, builder::PossibleValue};
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use std::fmt::Display;
#[derive(Clone, Debug, Default)]

View File

@ -65,7 +65,7 @@ impl ShardExec for OpenPGP {
"cert count {} != max {max}",
certs.len()
);
keyfork_shard::openpgp::split(threshold, certs, secret, output)
keyfork_shard::openpgp::split(threshold, certs, secret, output).map_err(Into::into)
}
fn combine<T>(

View File

@ -7,5 +7,7 @@ mod cli;
fn main() {
let opts = cli::Keyfork::parse();
opts.command.handle(&opts).expect("Unable to handle command");
opts.command
.handle(&opts)
.expect("Unable to handle command");
}

View File

@ -14,6 +14,7 @@ secp256k1 = ["keyfork-derive-util/secp256k1"]
bincode = "1.3.3"
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false }
keyfork-frame = { version = "0.1.0", path = "../keyfork-frame" }
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" }
thiserror = "1.0.49"
[dev-dependencies]

View File

@ -1,7 +1,7 @@
use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf};
use keyfork_derive_util::request::{DerivationRequest, DerivationResponse};
use keyfork_frame::{try_decode_from, try_encode_to, DecodeError, EncodeError};
use keyforkd_models::{Request, Response, Error as KeyforkdError};
#[cfg(test)]
mod tests;
@ -25,6 +25,9 @@ pub enum Error {
#[error("Could not perform frame transformation: {0}")]
FrameDec(#[from] DecodeError),
#[error("Error in Keyforkd: {0}")]
Keyforkd(#[from] KeyforkdError)
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -51,23 +54,32 @@ pub fn get_socket() -> Result<UnixStream, Error> {
UnixStream::connect(&socket_path).map_err(|e| Error::Connect(e, socket_path))
}
/// A client to interact with Keyforkd.
///
/// Upon creation, a socket is opened, and is kept open for the duration of the object's lifetime.
/// Currently, Keyforkd does not support the reuse of sockets. Attempting to reuse the socket after
/// previously using it will likely result in an error.
#[derive(Debug)]
pub struct Client {
socket: UnixStream,
}
impl Client {
/// Create a new client from a given already-connected [`UnixStream`].
pub fn new(socket: UnixStream) -> Self {
Self { socket }
}
/// Create a new client using well-known socket locations.
pub fn discover_socket() -> Result<Self> {
get_socket().map(|socket| Self { socket })
}
pub fn request(&mut self, req: &DerivationRequest) -> Result<DerivationResponse> {
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
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)?;
bincode::deserialize(&resp).map_err(From::from)
let resp: Result<Response, KeyforkdError> = bincode::deserialize(&resp)?;
resp.map_err(From::from)
}
}

View File

@ -17,7 +17,6 @@ fn secp256k1() {
let rt = Builder::new_multi_thread().enable_io().build().unwrap();
let tempdir = TempDir::new("keyfork-seed").unwrap();
for (i, per_seed) in tests.into_iter().enumerate() {
let mut socket_name = i.to_string();
socket_name.push_str("-keyforkd.sock");
let socket_path = tempdir.path().join(socket_name);
@ -47,7 +46,8 @@ fn secp256k1() {
DerivationAlgorithm::Secp256k1,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response = client.request(&req).unwrap();
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(response.data, test.private_key);
}
@ -57,15 +57,11 @@ fn secp256k1() {
#[test]
fn ed25519() {
let tests = test_data()
.unwrap()
.remove(&"ed25519".to_string())
.unwrap();
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
let rt = Builder::new_multi_thread().enable_io().build().unwrap();
let tempdir = TempDir::new("keyfork-seed").unwrap();
for (i, per_seed) in tests.into_iter().enumerate() {
let mut socket_name = i.to_string();
socket_name.push_str("-keyforkd.sock");
let socket_path = tempdir.path().join(socket_name);
@ -95,7 +91,8 @@ fn ed25519() {
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response = client.request(&req).unwrap();
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(response.data, test.private_key);
}

View File

@ -0,0 +1,11 @@
[package]
name = "keyforkd-models"
version = "0.1.0"
edition = "2021"
# 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 }
serde = { version = "1.0.190", features = ["derive"] }
thiserror = "1.0.50"

View File

@ -0,0 +1,84 @@
//! All values that can be sent to and returned from Keyforkd.
//!
//! All Error values are stored as enum unit values.
use keyfork_derive_util::request::{DerivationRequest, DerivationResponse};
use serde::{Deserialize, Serialize};
/// A request made to Keyforkd.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum Request {
/// A derivation request.
Derivation(DerivationRequest),
/// A derivation request, with a TTY provided for pinentry if necessary.
DerivationWithTTY(DerivationRequest, String),
}
impl From<DerivationRequest> for Request {
fn from(value: DerivationRequest) -> Self {
Self::Derivation(value)
}
}
impl From<(DerivationRequest, String)> for Request {
fn from((request, tty): (DerivationRequest, String)) -> Self {
Self::DerivationWithTTY(request, tty)
}
}
#[derive(thiserror::Error, Clone, Debug, Serialize, Deserialize)]
pub enum DerivationError {
#[error("The provided TTY was not valid")]
InvalidTTY,
#[error("A TTY was required by the pinentry program but was not provided")]
NoTTY,
#[error("Invalid derivation length: Expected 2, actual: {0}")]
InvalidDerivationLength(usize),
#[error("Derivation error: {0}")]
Derivation(String),
}
#[derive(thiserror::Error, Clone, Debug, Serialize, Deserialize)]
pub enum Error {
#[error(transparent)]
Derivation(#[from] DerivationError),
}
/// Any response from a Keyforkd request.
///
/// Responses can be converted to their inner values without matching the Response type by
/// using a type annotation and [`TryInto::try_into`].
#[derive(Serialize, Deserialize, Clone, Debug)]
#[non_exhaustive]
pub enum Response {
/// A derivation response.
///
/// From:
/// * [`Request::Derivation`]
/// * [`Request::DerivationWithTTY`]
Derivation(DerivationResponse),
}
#[derive(thiserror::Error, Debug)]
#[error("Unable to downcast to {0}")]
pub struct Downcast(&'static str);
use std::any::type_name;
impl TryFrom<Response> for DerivationResponse {
type Error = Downcast;
fn try_from(value: Response) -> Result<Self, Self::Error> {
#[allow(irrefutable_let_patterns)]
if let Response::Derivation(response) = value {
Ok(response)
} else {
Err(Downcast(type_name::<Self>()))
}
}
}

View File

@ -29,6 +29,7 @@ tower = { version = "0.4.13", features = ["tokio", "util"] }
thiserror = "1.0.47"
serde = { version = "1.0.186", features = ["derive"] }
keyfork-derive-path-data = { version = "0.1.0", path = "../keyfork-derive-path-data" }
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" }
[dev-dependencies]
hex-literal = "0.4.1"

View File

@ -59,7 +59,7 @@ where
S: Service<Request>,
Request: DeserializeOwned,
<S as Service<Request>>::Response: Serialize,
<S as Service<Request>>::Error: std::error::Error,
<S as Service<Request>>::Error: std::error::Error + Serialize,
<S as Service<Request>>::Future: Send + 'static,
{
type Response = Vec<u8>;
@ -86,9 +86,11 @@ where
let response = self.service.call(request);
Box::pin(async move {
let response = response
.await
.map_err(|e| BincodeServiceError::Call(e.to_string()))?;
let response = response.await;
#[cfg(feature = "tracing")]
if let Err(e) = &response {
tracing::error!("Error performing derivation: {e}");
}
serialize(&response).map_err(|e| BincodeServiceError::Convert(e.to_string()))
})
}
@ -116,7 +118,7 @@ mod tests {
struct App;
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, Serialize)]
enum Infallible {}
impl Service<Test> for App {
@ -141,7 +143,8 @@ mod tests {
#[tokio::test]
async fn can_serde_responses() {
let content = serialize(&Test::new()).unwrap();
let test = Test::new();
let content = serialize(&test).unwrap();
let mut service = ServiceBuilder::new()
.layer(BincodeLayer::<Test>::default())
.service(App);
@ -152,6 +155,6 @@ mod tests {
.call(content.clone())
.await
.unwrap();
assert_eq!(result, content);
assert_eq!(result, serialize(&Result::<Test, Infallible>::Ok(test)).unwrap());
}
}

View File

@ -1,22 +1,14 @@
use std::{future::Future, pin::Pin, sync::Arc, task::Poll};
use keyfork_derive_util::request::{DerivationError, DerivationRequest, DerivationResponse};
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;
// 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.
#[derive(thiserror::Error, Debug)]
pub enum KeyforkdRequestError {
#[error("Invalid derivation length: Expected: 2, actual: {0}")]
InvalidDerivationLength(usize),
#[error("Derivation error: {0}")]
Derivation(#[from] DerivationError),
}
//
#[derive(Clone, Debug)]
pub struct Keyforkd {
seed: Arc<Vec<u8>>,
@ -30,10 +22,11 @@ impl Keyforkd {
}
}
impl Service<DerivationRequest> for Keyforkd {
type Response = DerivationResponse;
impl Service<Request> for Keyforkd {
type Response = Response;
type Error = KeyforkdRequestError;
// TODO: indicate serialize in BincodeLayer
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
@ -45,24 +38,28 @@ impl Service<DerivationRequest> for Keyforkd {
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
fn call(&mut self, req: DerivationRequest) -> Self::Future {
fn call(&mut self, req: Request) -> Self::Future {
let seed = self.seed.clone();
Box::pin(async move {
let len = req.path().len();
if len < 2 {
return Err(KeyforkdRequestError::InvalidDerivationLength(len));
}
match req {
Request::Derivation(req) => Box::pin(async move {
let len = req.path().len();
if len < 2 {
return Err(DerivationError::InvalidDerivationLength(len).into());
}
#[cfg(feature = "tracing")]
if let Some(target) = guess_target(req.path()) {
info!("Deriving path: {target}");
} else {
info!("Deriving path: {}", req.path());
}
#[cfg(feature = "tracing")]
if let Some(target) = guess_target(req.path()) {
info!("Deriving path: {target}");
} else {
info!("Deriving path: {}", req.path());
}
req.derive_with_master_seed((*seed).clone())
.map_err(KeyforkdRequestError::from)
})
req.derive_with_master_seed((*seed).clone())
.map(Response::Derivation)
.map_err(|e| DerivationError::Derivation(e.to_string()).into())
}),
Request::DerivationWithTTY(_, _) => todo!(),
}
}
}
@ -92,7 +89,15 @@ mod tests {
continue;
}
let req = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response = keyforkd.ready().await.unwrap().call(req).await.unwrap();
let response: DerivationResponse = keyforkd
.ready()
.await
.unwrap()
.call(Request::Derivation(req))
.await
.unwrap()
.try_into()
.unwrap();
assert_eq!(response.data, test.private_key);
assert_eq!(response.chain_code.as_slice(), test.chain_code);
}
@ -101,10 +106,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".to_string()).unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -117,7 +119,15 @@ mod tests {
continue;
}
let req = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain);
let response = keyforkd.ready().await.unwrap().call(req).await.unwrap();
let response: DerivationResponse = keyforkd
.ready()
.await
.unwrap()
.call(Request::Derivation(req))
.await
.unwrap()
.try_into()
.unwrap();
assert_eq!(response.data, test.private_key);
assert_eq!(response.chain_code.as_slice(), test.chain_code);
}
@ -137,7 +147,15 @@ mod tests {
for (seed, path, _, private_key, _) in tests {
let req = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let mut keyforkd = Keyforkd::new(seed.to_vec());
let response = keyforkd.ready().await.unwrap().call(req).await.unwrap();
let response: DerivationResponse = keyforkd
.ready()
.await
.unwrap()
.call(Request::Derivation(req))
.await
.unwrap()
.try_into()
.unwrap();
assert_eq!(response.data, private_key)
}
}
@ -155,7 +173,15 @@ mod tests {
for (seed, path, _, private_key, _) in tests {
let req = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let mut keyforkd = Keyforkd::new(seed.to_vec());
let response = keyforkd.ready().await.unwrap().call(req).await.unwrap();
let response: DerivationResponse = keyforkd
.ready()
.await
.unwrap()
.call(Request::Derivation(req))
.await
.unwrap()
.try_into()
.unwrap();
assert_eq!(response.data, private_key)
}
}