From 96660a90e4cd296a2922d7a547de4cd9d0b1928b Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 1 Sep 2023 00:00:00 +0000 Subject: feat(login) password login using encrypted nsec Enables the user to only handle the nsec upon first use of the tool by encrypting it with a password and storing it on disk in an application cache. The approach to encryption draws heavily from that used by the gossip nostr client. - unencrypted nsec is zeroed from memory - a salt is used to defend against rainbow tables - computationally expensive key stretching defends against brute-force attacks of passwords with low entropy. There is UX trade-off between decryption speed and key-stretching computation. This UX challenge is exacerbated in a cli tool as decryption must take place more regularly. Thought was put into the selected n_log and a heavily reduced value is provided for long passwords where security benefits are smaller. A more granular reducing in computation was also considered by rejected to avoided to revealing just how weak a password is as most weak passwords are reused. --- Cargo.lock | 1908 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 8 + flake.nix | 18 +- planning.md | 78 ++ src/cli_interactor.rs | 31 +- src/config.rs | 111 ++- src/key_handling/encryption.rs | 247 ++++++ src/key_handling/mod.rs | 1 + src/key_handling/users.rs | 262 +++++- src/login.rs | 83 +- src/main.rs | 5 +- src/sub_commands/login.rs | 3 +- test_utils/Cargo.toml | 2 + test_utils/src/lib.rs | 116 ++- tests/login.rs | 334 ++++++- 15 files changed, 3052 insertions(+), 155 deletions(-) create mode 100644 planning.md create mode 100644 src/key_handling/encryption.rs diff --git a/Cargo.lock b/Cargo.lock index 3471bc3..994445e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,54 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.0.5" @@ -80,12 +128,219 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 1.9.0", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.23", + "slab", + "socket2 0.4.9", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 0.37.23", + "signal-hook", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-recursion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bip39" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" +dependencies = [ + "bitcoin_hashes 0.11.0", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e99ff7289b20a7385f66a0feda78af2fc119d28fb56aea8886a9cd0a4abdd75" +dependencies = [ + "bech32", + "bitcoin-private", + "bitcoin_hashes 0.12.0", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -98,6 +353,55 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding 0.2.1", + "cipher 0.3.0", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand 1.9.0", + "futures-lite", + "log", +] + [[package]] name = "bstr" version = "1.6.2" @@ -109,6 +413,33 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "cc" version = "1.0.83" @@ -124,6 +455,50 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "4.4.2" @@ -176,6 +551,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.7" @@ -189,6 +573,51 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -196,12 +625,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.0", "lock_api", "once_cell", "parking_lot_core", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "dialoguer" version = "0.10.4" @@ -220,6 +660,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "directories" version = "5.0.1" @@ -276,8 +727,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] -name = "errno" -version = "0.3.3" +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enumflags2" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ @@ -296,6 +783,21 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.0.0" @@ -311,6 +813,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fragile" version = "2.0.0" @@ -365,6 +882,32 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -386,6 +929,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -394,6 +938,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -401,10 +955,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.0" @@ -417,6 +1004,183 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding 0.3.3", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + [[package]] name = "itertools" version = "0.10.5" @@ -432,6 +1196,29 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9549a129bd08149e0a71b2d1ce2729780d47127991bfd0a78cc1df697ec72492" +dependencies = [ + "byteorder", + "lazy_static", + "linux-keyutils", + "secret-service", + "security-framework", + "winapi", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -444,6 +1231,22 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "linux-keyutils" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f27bb67f6dd1d0bb5ab582868e4f65052e58da6401188a08f0da09cf512b84b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.7" @@ -472,6 +1275,15 @@ version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.7.1" @@ -481,6 +1293,32 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mockall" version = "0.11.4" @@ -514,15 +1352,37 @@ version = "0.0.1" dependencies = [ "anyhow", "assert_cmd", + "chacha20poly1305", "clap", "dialoguer", "directories", "duplicate", + "keyring", "mockall", + "nostr", + "once_cell", + "passwords", + "rexpect 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "scrypt", "serde", "serde_json", "serial_test", "test_utils", + "zeroize", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", + "pin-utils", ] [[package]] @@ -534,7 +1394,7 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "memoffset 0.7.1", "pin-utils", ] @@ -544,6 +1404,96 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "nostr" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525a8f75106f4eeb1fedaacadc61547548fe4715c3edde7d03eed2900b467952" +dependencies = [ + "aes 0.8.3", + "base64", + "bech32", + "bip39", + "bitcoin", + "bitcoin_hashes 0.12.0", + "cbc", + "getrandom", + "instant", + "reqwest", + "secp256k1", + "serde", + "serde_json", + "tracing", + "url", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[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-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -553,17 +1503,48 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" [[package]] name = "parking_lot" @@ -588,6 +1569,42 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "passwords" +version = "3.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca743e019c2c679e1d92b329214cb4ffc4312200ca5ae60227aad1425c0aac8" +dependencies = [ + "random-pick", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -600,6 +1617,39 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "predicates" version = "2.1.5" @@ -642,6 +1692,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -666,6 +1726,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.66" @@ -684,6 +1750,67 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "random-number" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3da5cbb4c27c5150c03a54a7e4745437cd90f9e329ae657c0b889a144bb7be" +dependencies = [ + "proc-macro-hack", + "rand", + "random-number-macro-impl", +] + +[[package]] +name = "random-number-macro-impl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b86292cf41ccfc96c5de7165c1c53d5b4ac540c5bab9d1857acbe9eba5f1a0b" +dependencies = [ + "proc-macro-hack", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "random-pick" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c179499072da789afe44127d5f4aa6012de2c2f96ef759990196b37387a2a0f8" +dependencies = [ + "random-number", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -742,18 +1869,106 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "reqwest" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "tokio-socks", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "rexpect" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ff60778f96fb5a48adbe421d21bf6578ed58c0872d712e7e08593c195adff8" +dependencies = [ + "comma", + "nix 0.25.1", + "regex", + "tempfile", + "thiserror", +] + [[package]] name = "rexpect" version = "0.5.0" source = "git+https://github.com/phaer/rexpect.git?branch=skip-ansi-escape-codes#f099dc4750e38a1c31d2ab06d7d3e0352679d85a" dependencies = [ "comma", - "nix", + "nix 0.26.4", "regex", "tempfile", "thiserror", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.13" @@ -763,22 +1978,147 @@ dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.7", "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes 0.12.0", + "rand", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + +[[package]] +name = "secret-service" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da1a5ad4d28c03536f82f77d9f36603f5e37d8869ac98f0a750d5b5686d8d95" +dependencies = [ + "aes 0.7.5", + "block-modes", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand", + "serde", + "sha2", + "zbus", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.188" @@ -810,6 +2150,29 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serial_test" version = "2.0.0" @@ -835,12 +2198,53 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shell-words" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.9" @@ -851,10 +2255,42 @@ dependencies = [ ] [[package]] -name = "smallvec" -version = "1.11.0" +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strip-ansi-escapes" @@ -871,6 +2307,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -900,9 +2342,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", - "fastrand", + "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix", + "rustix 0.38.13", "windows-sys 0.48.0", ] @@ -920,7 +2362,9 @@ dependencies = [ "assert_cmd", "dialoguer", "directories", - "rexpect", + "nostr", + "once_cell", + "rexpect 0.5.0 (git+https://github.com/phaer/rexpect.git?branch=skip-ansi-escape-codes)", "strip-ansi-escapes", ] @@ -944,18 +2388,204 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.5.3", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap 2.0.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -997,12 +2627,131 @@ dependencies = [ "libc", ] +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.32", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.45.0" @@ -1135,8 +2884,141 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winnow" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "xdg-home" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +dependencies = [ + "nix 0.26.4", + "winapi", +] + +[[package]] +name = "zbus" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.26.4", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zeroize" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zvariant" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml index e745441..6d8ebaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,16 +13,24 @@ categories = ["command-line-utilities","git"] [dependencies] anyhow = "1.0.75" +chacha20poly1305 = "0.10.1" clap = { version = "4.3.19", features = ["derive"] } dialoguer = "0.10.4" directories = "5.0.1" +keyring = "2.0.5" +nostr = "0.23.0" +passwords = "3.1.13" +scrypt = "0.11.0" serde = { version = "1.0.181", features = ["derive"] } serde_json = "1.0.105" +zeroize = "1.6.0" [dev-dependencies] assert_cmd = "2.0.12" duplicate = "1.0.0" mockall = "0.11.4" +once_cell = "1.18.0" +rexpect = "0.5.0" serial_test = "2.0.0" test_utils = { path = "test_utils" } diff --git a/flake.nix b/flake.nix index 7c36e2d..2fa8d8a 100644 --- a/flake.nix +++ b/flake.nix @@ -18,19 +18,13 @@ devShells.default = mkShell { nativeBuildInputs = [ - # stable to be introduced when the following issue is resolved + # override rustfmt with nightly toolchain version to support unstable features + # ideally this wouldn't be pinned to a specific nightly version but + # selectLatestNightlyWith isn't support with mixed toolchains # https://github.com/oxalica/rust-overlay/issues/136 - # rust-bin.stable.latest.default - # nightly for rustfmt - ( - rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override { - extensions = [ - "rust-src" - "rustfmt" - "clippy" - ]; - }) - ) + (lib.hiPrio rust-bin.nightly."2023-09-01".rustfmt) + rust-bin.stable.latest.default + ]; buildInputs = [ diff --git a/planning.md b/planning.md new file mode 100644 index 0000000..d17d176 --- /dev/null +++ b/planning.md @@ -0,0 +1,78 @@ +/* ! + +# Authentication and Key Management Requirements + +## User Experience + +For a smooth UX: +1. a private key should only need to be imported once +2. authentication to sign events should persist at least across multiple calls +to the cli tool within a single terminal session. + +## Security + +1. key material must be encrypted with a salted passphrase when stored on disk. +2. the passphase should only be accessable + a) by this specific cli tool; or alternatively + b) only within the terminal session + + +# Implementation + +Every private key entired into the tool is encrypted with a salted user +provided passphrase and stored on disk in the tool's configuration file +alongside display_name and public key for identification. + +The private key of the current logged-in user is encrypted with a salted +randomly generated token and stored on disk in the configuration file alongside +the public key for identification. The token is stored in the OS's keyring +using a rust crate called 'keyring'. On Linux this expires after a few days +whilst on Windows and MacOS it never expires. + +Should the token be cycled? cycling the token would prevent an attacker who had +access to only the token or the encrypted key from returning after the token +had been cycled. This isn't worth it. An attacker is much more likely to have +access to both simultainiously. + +logout should delete the key encrypted with the token and the token. It should +give the option to clear encrypted key material for the current user or all +users. + +*/ + +init + +initialize repoisiotr + + +replaceable event + +commit id + +search by initial commit / initial 5 commits +name + + + +initialising a reposistory + + +git nostr init + > intialise repo + + +git nostr init - request patches / PRs, issues, + features to support + -- branch + -- patches / PRs + -- issues + + -- override git push to also push to nostr. + + settings + --git-repos - one or more git repositories where the latest commits can be pulled from + --name + --description + + +git push nostr main diff --git a/src/cli_interactor.rs b/src/cli_interactor.rs index 2f28aee..d7de087 100644 --- a/src/cli_interactor.rs +++ b/src/cli_interactor.rs @@ -1,5 +1,5 @@ -use anyhow::{bail, Result}; -use dialoguer::{theme::ColorfulTheme, Input}; +use anyhow::Result; +use dialoguer::{theme::ColorfulTheme, Input, Password}; #[cfg(test)] use mockall::*; @@ -11,6 +11,7 @@ pub struct Interactor { #[cfg_attr(test, automock)] pub trait InteractorPrompt { fn input(&self, parms: PromptInputParms) -> Result; + fn password(&self, parms: PromptPasswordParms) -> Result; } impl InteractorPrompt for Interactor { fn input(&self, parms: PromptInputParms) -> Result { @@ -19,6 +20,15 @@ impl InteractorPrompt for Interactor { .interact_text()?; Ok(input) } + fn password(&self, parms: PromptPasswordParms) -> Result { + let mut p = Password::with_theme(&self.theme); + p.with_prompt(parms.prompt); + if parms.confirm { + p.with_confirmation("confirm password", "passwords didnt match..."); + } + let pass: String = p.interact()?; + Ok(pass) + } } #[derive(Default)] @@ -32,3 +42,20 @@ impl PromptInputParms { self } } + +#[derive(Default)] +pub struct PromptPasswordParms { + pub prompt: String, + pub confirm: bool, +} + +impl PromptPasswordParms { + pub fn with_prompt>(mut self, prompt: S) -> Self { + self.prompt = prompt.into(); + self + } + pub const fn with_confirm(mut self) -> Self { + self.confirm = true; + self + } +} diff --git a/src/config.rs b/src/config.rs index b26dea0..f410934 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, Context, Result}; use directories::ProjectDirs; #[cfg(test)] use mockall::*; +use nostr::secp256k1::XOnlyPublicKey; use serde::{self, Deserialize, Serialize}; #[derive(Default)] @@ -59,7 +60,7 @@ impl ConfigManagement for ConfigManager { } } -#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)] #[allow(clippy::module_name_repetitions)] pub struct MyConfig { pub version: u8, @@ -68,44 +69,64 @@ pub struct MyConfig { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct UserRef { - pub nsec: String, + pub public_key: XOnlyPublicKey, + pub encrypted_key: String, } #[cfg(test)] mod tests { use anyhow::Result; use serial_test::serial; - use test_utils::*; use super::*; + fn backup_existing_config() -> Result<()> { + let config_path = get_dirs()?.config_dir().join("config.json"); + let backup_config_path = get_dirs()?.config_dir().join("config-backup.json"); + if config_path.exists() { + std::fs::rename(config_path, backup_config_path)?; + } + Ok(()) + } + + fn restore_config_backup() -> Result<()> { + let config_path = get_dirs()?.config_dir().join("config.json"); + let backup_config_path = get_dirs()?.config_dir().join("config-backup.json"); + if config_path.exists() { + std::fs::remove_file(&config_path)?; + } + if backup_config_path.exists() { + std::fs::rename(backup_config_path, config_path)?; + } + Ok(()) + } + mod load { use super::*; #[test] #[serial] fn when_config_file_doesnt_exist_defaults_are_returned() -> Result<()> { - with_fresh_config(|| { - assert_eq!(ConfigManager.load()?, MyConfig::default()); - - Ok(()) - }) + backup_existing_config()?; + let c = ConfigManager; + assert_eq!(c.load()?, MyConfig::default()); + restore_config_backup()?; + Ok(()) } #[test] #[serial] fn when_config_file_exists_it_is_returned() -> Result<()> { - with_fresh_config(|| { - let c = ConfigManager; - let new_config = MyConfig { - version: 255, - ..MyConfig::default() - }; - c.save(&new_config)?; - assert_eq!(c.load()?, new_config); - - Ok(()) - }) + backup_existing_config()?; + let c = ConfigManager; + let new_config = MyConfig { + version: 255, + ..MyConfig::default() + }; + c.save(&new_config)?; + assert_eq!(c.load()?, new_config); + restore_config_backup()?; + Ok(()) } } @@ -115,38 +136,36 @@ mod tests { #[test] #[serial] fn when_config_file_doesnt_config_is_saved() -> Result<()> { - with_fresh_config(|| { - let c = ConfigManager; - let new_config = MyConfig { - version: 255, - ..MyConfig::default() - }; - c.save(&new_config)?; - assert_eq!(c.load()?, new_config); - - Ok(()) - }) + backup_existing_config()?; + let c = ConfigManager; + let new_config = MyConfig { + version: 255, + ..MyConfig::default() + }; + c.save(&new_config)?; + assert_eq!(c.load().unwrap(), new_config); + restore_config_backup()?; + Ok(()) } #[test] #[serial] fn when_config_file_exists_new_config_is_saved() -> Result<()> { - with_fresh_config(|| { - let c = ConfigManager; - let config = MyConfig { - version: 255, - ..MyConfig::default() - }; - c.save(&config)?; - let new_config = MyConfig { - version: 254, - ..MyConfig::default() - }; - c.save(&new_config)?; - assert_eq!(c.load()?, new_config); - - Ok(()) - }) + backup_existing_config()?; + let c = ConfigManager; + let config = MyConfig { + version: 255, + ..MyConfig::default() + }; + c.save(&config)?; + let new_config = MyConfig { + version: 254, + ..MyConfig::default() + }; + c.save(&new_config)?; + assert_eq!(c.load().unwrap(), new_config); + restore_config_backup()?; + Ok(()) } } } diff --git a/src/key_handling/encryption.rs b/src/key_handling/encryption.rs new file mode 100644 index 0000000..0ef7f69 --- /dev/null +++ b/src/key_handling/encryption.rs @@ -0,0 +1,247 @@ +use anyhow::{anyhow, bail, ensure, Context, Result}; +use chacha20poly1305::{ + aead::{rand_core::RngCore, Aead, AeadCore, KeyInit, OsRng, Payload}, + XChaCha20Poly1305, +}; +#[cfg(test)] +use mockall::*; +use nostr::{prelude::*, Keys}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use zeroize::Zeroize; + +#[derive(Default)] +pub struct Encryptor; + +#[cfg_attr(test, automock)] +pub trait EncryptDecrypt { + /// requires less CPU time if the password is long + fn encrypt_key(&self, keys: &Keys, password: &str) -> Result; + fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result; + /// generates a long random string + fn random_token(&self) -> String; +} + +/// approach and code adapted from nostr gossip client +impl EncryptDecrypt for Encryptor { + fn encrypt_key(&self, keys: &Keys, password: &str) -> Result { + // Generate a random 16-byte salt + let salt = { + let mut salt: [u8; 16] = [0; 16]; + OsRng.fill_bytes(&mut salt); + salt + }; + + let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); + + let log2_rounds: u8 = if password.len() > 20 { + // we have enough of entropy - no need to spend CPU time adding much more + 1 + } else { + // default (scrypt::Params::RECOMMENDED_LOG_N) is 17 but 30s is too long to wait + 15 + }; + + let associated_data: Vec = vec![1]; + + let ciphertext = { + let cipher = { + let symmetric_key = password_to_key(password, &salt, log2_rounds) + .context("failed create encryption key from password")?; + XChaCha20Poly1305::new((&symmetric_key).into()) + }; + cipher + .encrypt( + &nonce, + Payload { + msg: keys + .secret_key() + .context( + "supplied key should reveal secret key. Is this a public key only?", + )? + .display_secret() + .to_string() + .as_bytes(), + aad: &associated_data, + }, + ) + .map_err(|_| anyhow!("ChaChaPoly1305 failed to encrypt nsec with password"))? + }; + // Combine salt, IV and ciphertext + let mut concatenation: Vec = Vec::new(); + concatenation.push(0x1); // 1 byte version number + concatenation.push(log2_rounds); // 1 byte for scrypt N (rounds) + concatenation.extend(salt); // 16 bytes of salt + concatenation.extend(nonce); // 24 bytes of nonce + concatenation.extend(associated_data); // 1 byte of key security + concatenation.extend(ciphertext); // 48 bytes of ciphertext expected + // Total length is 91 = 1 + 1 + 16 + 24 + 1 + 48 + + bech32::encode( + "ncryptsec", + concatenation.to_base32(), + bech32::Variant::Bech32, + ) + .context("encrypted nsec failed to encode") + } + + fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result { + let data = + bech32::decode(encrypted_key).context("failed to decode encrypted key as bech32")?; + if data.0 != "ncryptsec" { + bail!("encrypted key is in the wrong format - it doesnt start with ncryptsec"); + } + let concatenation = Vec::::from_base32(&data.1) + .context("failed to convert bech32::decode output to Vec")?; + + // Break into parts + let version: u8 = concatenation[0]; + ensure!(version == 0x1, "encryption version is incorrect"); + let log2_rounds: u8 = concatenation[1]; + let salt: [u8; 16] = concatenation[2..2 + 16].try_into()?; + let nonce = &concatenation[2 + 16..2 + 16 + 24]; + let associated_data = &concatenation[(2 + 16 + 24)..=(2 + 16 + 24)]; + let ciphertext = &concatenation[2 + 16 + 24 + 1..]; + + let cipher = { + let symmetric_key = password_to_key(password, &salt, log2_rounds)?; + XChaCha20Poly1305::new((&symmetric_key).into()) + }; + + let payload = Payload { + msg: ciphertext, + aad: associated_data, + }; + + let mut inner_secret = cipher + .decrypt(nonce.into(), payload) + .map_err(|_| anyhow!("failed to decrypt"))?; + + if associated_data.is_empty() { + bail!("invalid encrypted key"); + } + + let key = Keys::from_sk_str( + std::str::from_utf8(&inner_secret).context("inner secret is not [u8]")?, + ) + .context("incorrect password. Key decrypted with password did not produce a valid nsec.")?; + + inner_secret.zeroize(); + + Ok(key) + } + + fn random_token(&self) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect() + } +} + +/// uses scrypt to stretch password into key +fn password_to_key(password: &str, salt: &[u8; 16], log_n: u8) -> Result<[u8; 32]> { + let params = scrypt::Params::new(log_n, 8, 1, 32) + .context("scrypt failed to generate params to stretch password")?; + let mut key: [u8; 32] = [0; 32]; + if log_n > 14 { + println!("this may take a few seconds..."); + } + + scrypt::scrypt(password.as_bytes(), salt, ¶ms, &mut key) + .context("scrypt failed to stretch password")?; + Ok(key) +} + +#[cfg(test)] +mod tests { + use test_utils::*; + + use super::*; + + #[test] + fn encrypt_key_produces_string_prefixed_with() -> Result<()> { + let s = Encryptor.encrypt_key(&nostr::Keys::generate(), TEST_PASSWORD)?; + assert!(s.starts_with("ncryptsec")); + Ok(()) + } + + #[test] + // ensures password encryption hasn't changed + fn decrypts_with_strong_password_from_reference_string() -> Result<()> { + let encryptor = Encryptor; + let decrypted_key = encryptor.decrypt_key(TEST_KEY_1_ENCRYPTED, TEST_PASSWORD)?; + + assert_eq!( + format!( + "{}", + TEST_KEY_1_KEYS.secret_key().unwrap().to_bech32().unwrap() + ), + format!( + "{}", + decrypted_key.secret_key().unwrap().to_bech32().unwrap() + ), + ); + Ok(()) + } + + #[test] + // ensures password encryption hasn't changed + fn decrypts_with_weak_password_from_reference_string() -> Result<()> { + let encryptor = Encryptor; + let decrypted_key = encryptor.decrypt_key(TEST_KEY_1_ENCRYPTED_WEAK, TEST_WEAK_PASSWORD)?; + + assert_eq!( + format!( + "{}", + TEST_KEY_1_KEYS.secret_key().unwrap().to_bech32().unwrap() + ), + format!( + "{}", + decrypted_key.secret_key().unwrap().to_bech32().unwrap() + ), + ); + Ok(()) + } + + #[test] + fn decrypts_key_encrypted_using_encrypt_key() -> Result<()> { + let encryptor = Encryptor; + let key = nostr::Keys::generate(); + let s = encryptor.encrypt_key(&key, TEST_PASSWORD)?; + let newkey = encryptor.decrypt_key(s.as_str(), TEST_PASSWORD)?; + + assert_eq!( + format!("{}", key.secret_key().unwrap().to_bech32().unwrap()), + format!("{}", newkey.secret_key().unwrap().to_bech32().unwrap()), + ); + Ok(()) + } + + #[test] + fn decrypt_key_successfully_decrypts_key_encrypted_using_encrypt_key() -> Result<()> { + let encryptor = Encryptor; + let key = nostr::Keys::generate(); + let s = encryptor.encrypt_key(&key, TEST_PASSWORD)?; + let newkey = encryptor.decrypt_key(s.as_str(), TEST_PASSWORD)?; + + assert_eq!( + format!("{}", key.secret_key().unwrap().to_bech32().unwrap()), + format!("{}", newkey.secret_key().unwrap().to_bech32().unwrap()), + ); + Ok(()) + } + + #[test] + fn password_to_key_returns_ok_with_standard_password() { + let salt = { + let mut salt: [u8; 16] = [0; 16]; + OsRng.fill_bytes(&mut salt); + salt + }; + + let log2_rounds: u8 = 1; + + assert!(password_to_key(TEST_PASSWORD, &salt, log2_rounds).is_ok()); + } +} diff --git a/src/key_handling/mod.rs b/src/key_handling/mod.rs index 913bd46..bcb10df 100644 --- a/src/key_handling/mod.rs +++ b/src/key_handling/mod.rs @@ -1 +1,2 @@ +pub mod encryption; pub mod users; diff --git a/src/key_handling/users.rs b/src/key_handling/users.rs index bd1748a..1d2cc34 100644 --- a/src/key_handling/users.rs +++ b/src/key_handling/users.rs @@ -1,47 +1,90 @@ use anyhow::{Context, Result}; +use nostr::prelude::*; +use zeroize::Zeroize; +use super::encryption::{EncryptDecrypt, Encryptor}; use crate::{ - cli_interactor::{Interactor, InteractorPrompt, PromptInputParms}, - config::{ConfigManagement, ConfigManager, MyConfig, UserRef}, + cli_interactor::{Interactor, InteractorPrompt, PromptInputParms, PromptPasswordParms}, + config::{self, ConfigManagement, ConfigManager}, }; #[derive(Default)] pub struct UserManager { config_manager: ConfigManager, interactor: Interactor, + encryptor: Encryptor, } pub trait UserManagement { - fn add(&self, nsec: &Option) -> Result<()>; + fn add(&self, nsec: &Option, password: &Option) -> Result; } #[cfg(test)] use duplicate::duplicate_item; #[cfg_attr(test, duplicate_item(UserManager; [UserManager]; [self::tests::MockUserManager]))] impl UserManagement for UserManager { - fn add(&self, nsec: &Option) -> Result<()> { - let nsec = match nsec.clone() { - Some(nsec) => nsec, + fn add(&self, nsec: &Option, password: &Option) -> Result { + let mut prompt = "login with nsec (or hex private key)"; + let keys = loop { + let pk = match nsec.clone() { + Some(nsec) => nsec, + None => self + .interactor + .input(PromptInputParms::default().with_prompt(prompt)) + .context("failed to get nsec input from interactor")?, + }; + match Keys::from_sk_str(&pk) { + Ok(key) => { + break key; + } + Err(e) => { + if nsec.is_some() { + return Err(e).context( + "invalid nsec - supplied parameter could not be converted into a nostr private key", + ); + } + prompt = "invalid nsec. try again with nsec (or hex private key)"; + } + } + }; + + let mut pass = match password.clone() { + Some(pass) => pass, None => self .interactor - .input( - PromptInputParms::default().with_prompt("login with nsec (or hex private key)"), + .password( + PromptPasswordParms::default() + .with_prompt("encrypt with password") + .with_confirm(), ) - .context("failed to get nsec input from interactor.input")?, + .context("failed to get password input from interactor.password")?, }; + let encrypted_secret_key = self + .encryptor + .encrypt_key(&keys, &pass) + .context("failed to encrypt nsec with password.")?; + pass.zeroize(); + + let user_ref = config::UserRef { + public_key: keys.public_key(), + encrypted_key: encrypted_secret_key, + }; + + // remove any duplicate entries for key before adding it to config + let mut cfg = self.config_manager.load().context("failed to load application config to find and remove any old versions of the user's encrypted key")?; + cfg.users = cfg + .users + .clone() + .into_iter() + .filter(|r| !r.public_key.eq(&keys.public_key())) + .collect(); + cfg.users.push(user_ref); self.config_manager - .save(&MyConfig { - users: vec![UserRef { - nsec: nsec.to_string(), - }], - ..MyConfig::default() - }) + .save(&cfg) .context("failed to save application configuration with new user details in")?; - println!("logged in as {nsec}"); - - Ok(()) + Ok(keys) } } @@ -50,12 +93,17 @@ mod tests { use test_utils::*; use super::*; - use crate::{cli_interactor::MockInteractorPrompt, config::MockConfigManagement}; + use crate::{ + cli_interactor::MockInteractorPrompt, + config::{MockConfigManagement, MyConfig, UserRef}, + key_handling::encryption::MockEncryptDecrypt, + }; #[derive(Default)] pub struct MockUserManager { pub config_manager: MockConfigManagement, pub interactor: MockInteractorPrompt, + pub encryptor: MockEncryptDecrypt, } mod add { @@ -70,28 +118,88 @@ mod tests { self.interactor .expect_input() .returning(|_| Ok(TEST_KEY_1_NSEC.into())); + self.interactor + .expect_password() + .returning(|_| Ok(TEST_PASSWORD.into())); + self.encryptor + .expect_encrypt_key() + .returning(|_, _| Ok(TEST_KEY_1_ENCRYPTED.into())); self } } - mod when_nsec_is_passed { + fn reuable_user_isnt_prompted(nsec: &str) { + let mut m = MockUserManager::default().add_return_expected_responses(); + m.interactor = MockInteractorPrompt::default(); + m.interactor.expect_input().never(); + m.interactor.expect_password().never(); + let _ = m.add(&Some(nsec.into()), &Some(TEST_PASSWORD.to_string())); + } + + fn reuable_config_isnt_modified(nsec: &str) { + let mut m = MockUserManager::default(); + m.config_manager.expect_save().never(); + let _ = m.add(&Some(nsec.into()), &Some(TEST_PASSWORD.to_string())); + } + + mod when_valid_nsec_and_password_is_passed { use super::*; #[test] fn user_isnt_prompted() { + reuable_user_isnt_prompted(TEST_KEY_1_NSEC); + } + + #[test] + fn results_in_correct_keys() { let mut m = MockUserManager::default().add_return_expected_responses(); m.interactor = MockInteractorPrompt::default(); m.interactor.expect_input().never(); - - let _ = m.add(&Some(TEST_KEY_1_NSEC.into())); + m.interactor.expect_password().never(); + let r = m.add( + &Some(TEST_KEY_1_NSEC.into()), + &Some(TEST_PASSWORD.to_string()), + ); + assert!(r.is_ok(), "should result in keys"); + assert!( + r.is_ok_and(|k| k + .secret_key() + .is_ok_and(|k| k.display_secret().to_string().eq(TEST_KEY_1_SK_HEX))), + "keys should reflect nsec" + ); } } + mod when_invalid_nsec_is_passed_with_password { + use super::*; + #[test] + fn user_isnt_prompted() { + reuable_user_isnt_prompted(TEST_INVALID_NSEC); + } + + #[test] + fn config_isnt_modified() { + reuable_config_isnt_modified(TEST_INVALID_NSEC); + } + + #[test] + fn results_in_an_error() { + let m = MockUserManager::default(); + assert!( + m.add( + &Some(TEST_INVALID_NSEC.into()), + &Some(TEST_PASSWORD.to_string()) + ) + .is_err(), + "should result in an error" + ); + } + } mod when_no_nsec_is_passed { use super::*; #[test] - fn prompt_for_nsec() { + fn prompt_for_nsec_and_password() { let mut m = MockUserManager::default().add_return_expected_responses(); m.interactor = MockInteractorPrompt::new(); @@ -100,12 +208,31 @@ mod tests { .once() .withf(|p| p.prompt.eq("login with nsec (or hex private key)")) .returning(|_| Ok(TEST_KEY_1_NSEC.into())); + m.interactor + .expect_password() + .once() + .withf(|p| p.prompt.eq("encrypt with password")) + .returning(|_| Ok(TEST_KEY_1_NSEC.into())); - let _ = m.add(&None); + let _ = m.add(&None, &None); } #[test] - fn stored_in_config() { + fn results_in_correct_keys() { + let m = MockUserManager::default().add_return_expected_responses(); + + let r = m.add(&None, &None); + assert!(r.is_ok(), "should result in keys"); + assert!( + r.is_ok_and(|k| k + .secret_key() + .is_ok_and(|k| k.display_secret().to_string().eq(TEST_KEY_1_SK_HEX))), + "keys should reflect nsec" + ); + } + + #[test] + fn stores_encrypted_key_in_config() { let mut m = MockUserManager::default().add_return_expected_responses(); m.config_manager = MockConfigManagement::new(); @@ -114,10 +241,91 @@ mod tests { .returning(|| Ok(MyConfig::default())); m.config_manager .expect_save() - .withf(|cfg| cfg.users.len().eq(&1) && cfg.users[0].nsec.eq(TEST_KEY_1_NSEC)) + .withf(|cfg| { + cfg.users.len().eq(&1) + && cfg.users[0].encrypted_key.eq(TEST_KEY_1_ENCRYPTED) + }) .returning(|_| Ok(())); - let _ = m.add(&None); + let _ = m.add(&None, &None); + } + + #[test] + fn stored_key_encrypted_with_password() { + let mut m = MockUserManager::default().add_return_expected_responses(); + + m.encryptor = MockEncryptDecrypt::new(); + m.encryptor + .expect_encrypt_key() + .once() + .withf(|k, p| { + k.eq(&Keys::from_sk_str(TEST_KEY_1_NSEC).unwrap()) && p.eq(TEST_PASSWORD) + }) + .returning(|_, _| Ok(TEST_KEY_1_ENCRYPTED.into())); + + let _ = m.add(&None, &None); + } + + mod when_user_key_already_stored { + use super::*; + use crate::config::UserRef; + + /// key overwritten as password may have changed + #[test] + fn key_not_saved_as_duplicate_but_encrypted_key_overwritten() { + let mut m = MockUserManager::default().add_return_expected_responses(); + + m.config_manager = MockConfigManagement::default(); + m.config_manager.expect_load().returning(|| { + Ok(MyConfig { + users: vec![UserRef { + public_key: TEST_KEY_1_KEYS.public_key(), + // different key to TEST_KEY_1_ENCYPTED + encrypted_key: TEST_KEY_2_ENCRYPTED.into(), + }], + ..MyConfig::default() + }) + }); + m.config_manager + .expect_save() + .withf(|cfg| { + cfg.users.len() == 1 + && cfg.users[0].encrypted_key.eq(TEST_KEY_1_ENCRYPTED) + }) + .returning(|_| Ok(())); + + let _ = m.add(&None, &None); + } + } + + mod when_multiple_users_added { + use super::*; + + #[test] + fn both_user_keys_are_stored() { + let mut m = MockUserManager::default().add_return_expected_responses(); + + m.config_manager = MockConfigManagement::default(); + m.config_manager.expect_load().returning(|| { + Ok(MyConfig { + users: vec![UserRef { + public_key: TEST_KEY_2_KEYS.public_key(), + encrypted_key: TEST_KEY_2_ENCRYPTED.into(), + }], + ..MyConfig::default() + }) + }); + m.config_manager + .expect_save() + .withf(|cfg| { + cfg.users.len() == 2 + // latest user stored at end of array + && cfg.users[1].encrypted_key.eq(TEST_KEY_1_ENCRYPTED) + }) + .returning(|_| Ok(())); + + let _ = m.add(&None, &None); + } } } } diff --git a/src/login.rs b/src/login.rs index da19a75..a6ce76d 100644 --- a/src/login.rs +++ b/src/login.rs @@ -1,16 +1,85 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; +use nostr::prelude::{FromSkStr, ToBech32}; +use zeroize::Zeroize; use crate::{ + cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, config::{ConfigManagement, ConfigManager}, - key_handling::users::{UserManagement, UserManager}, + key_handling::{ + encryption::{EncryptDecrypt, Encryptor}, + users::{UserManagement, UserManager}, + }, }; -pub fn launch(nsec: &Option) -> Result<()> { +/// handles the encrpytion and storage of key material +pub fn launch(nsec: &Option, password: &Option) -> Result { + // if nsec parameter + if let Some(nsec_unwrapped) = nsec { + // get key or fail without prompts + let key = nostr::Keys::from_sk_str(nsec_unwrapped).context("invalid nsec parameter")?; + println!( + "logged in as {}", + &key.public_key() + .to_bech32() + .context("public key should always produce bech32")? + ); + + // if password, add user to enable password login in future + if password.is_some() { + UserManager::default() + .add(nsec, password) + .context("could not store identity")?; + } + return Ok(key); + } + + // if encrypted nsec stored, attempt password let cfg = ConfigManager .load() .context("failed to load application config")?; - if !cfg.users.is_empty() { - println!("logged in as {}", cfg.users[0].nsec); - } - UserManager::default().add(nsec) + let key = if let Some(user) = cfg.users.last() { + let mut pass = if let Some(p) = password.clone() { + p + } else { + println!( + "login as {}", + &user + .public_key + .to_bech32() + .context("public key should always produce bech32")? + ); + Interactor::default() + .password(PromptPasswordParms::default().with_prompt("password")) + .context("failed to get password input from interactor.password")? + }; + + let key_result = Encryptor + .decrypt_key(&user.encrypted_key, pass.as_str()) + .context("failed to decrypt key with provided password"); + pass.zeroize(); + + key_result.context(format!( + "failed to log in as {}", + &user + .public_key + .to_bech32() + .context("public key should always produce bech32")? + ))? + } else { + // no nsec but password supplied + if password.is_some() { + bail!("no nsec available to decrypt with specified password"); + } + // otherwise add new user with nsec and password prompts + UserManager::default() + .add(nsec, password) + .context("failed to add user")? + }; + println!( + "logged in as {}", + &key.public_key() + .to_bech32() + .context("public key should always produce bech32")? + ); + Ok(key) } diff --git a/src/main.rs b/src/main.rs index d16f1a3..e6eac32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,8 +17,11 @@ pub struct Cli { #[command(subcommand)] command: Commands, /// nsec or hex private key - #[arg(short, long)] + #[arg(short, long, global = true)] nsec: Option, + /// password to decrypt nsec + #[arg(short, long, global = true)] + password: Option, } #[derive(Subcommand)] diff --git a/src/sub_commands/login.rs b/src/sub_commands/login.rs index d61f578..5391024 100644 --- a/src/sub_commands/login.rs +++ b/src/sub_commands/login.rs @@ -7,5 +7,6 @@ use crate::{login, Cli}; pub struct SubCommandArgs; pub fn launch(args: &Cli, _command_args: &SubCommandArgs) -> Result<()> { - login::launch(&args.nsec) + let _ = login::launch(&args.nsec, &args.password)?; + Ok(()) } diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml index e1f6090..1a39957 100644 --- a/test_utils/Cargo.toml +++ b/test_utils/Cargo.toml @@ -8,5 +8,7 @@ anyhow = "1.0.75" assert_cmd = "2.0.12" dialoguer = "0.10.4" directories = "5.0.1" +nostr = "0.23.0" +once_cell = "1.18.0" rexpect = { git = "https://github.com/phaer/rexpect.git", branch= "skip-ansi-escape-codes" } strip-ansi-escapes = "0.2.0" diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 495e8d2..1a4231a 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -3,14 +3,38 @@ use std::ffi::OsStr; use anyhow::{ensure, Context, Result}; use dialoguer::theme::{ColorfulTheme, Theme}; use directories::ProjectDirs; +use nostr::{self, prelude::FromSkStr}; +use once_cell::sync::Lazy; use rexpect::session::{Options, PtySession}; use strip_ansi_escapes::strip_str; pub static TEST_KEY_1_NSEC: &str = "nsec1ppsg5sm2aexq06juxmu9evtutr6jkwkhp98exxxvwamhru9lyx9s3rwseq"; +pub static TEST_KEY_1_SK_HEX: &str = + "08608a436aee4c07ea5c36f85cb17c58f52b3ad7094f9318cc777771f0bf218b"; +pub static TEST_KEY_1_NPUB: &str = + "npub175lyhnt6nn00qjw0v3navw9pxgv43txnku0tpxprl4h6mvpr6a5qlphudg"; +pub static TEST_KEY_1_DISPLAY_NAME: &str = "bob"; +pub static TEST_KEY_1_ENCRYPTED: &str = "ncryptsec1qyq607h3cykxc3f2a44u89cdk336fptccn3fm5pf3nmf93d3c86qpunc7r6klwcn6lyszjy72wxwqq9aljg4pm6atvjrds9e248yhv76xfnt464265kgnjsvg8rlg06wg4sp9uljzfpu8zuaztcvfn2j8ggdrg8mldh850cy75efsyqqansert9wqmn4e6khpgvfz7h5le9"; +pub static TEST_KEY_1_ENCRYPTED_WEAK: &str = "ncryptsec1qy8ke0tjqnn8wt3w6lnc86c27ry3qrptxctjfcgruryxy0at238kwyjwsswd7z88thysruzw3awlrsxjvw5uptcd7vt70ft9rtkx00m8cgy3khm4hxa5d2gfnc6athnfruy2eyl6pkas8k34jg85z7xjqqadzfzh9rp0fzxqtw0tvxksac3n8yc98uksvuf93e0lcvqy8j6"; +pub static TEST_KEY_1_KEYS: Lazy = + Lazy::new(|| nostr::Keys::from_sk_str(TEST_KEY_1_NSEC).unwrap()); pub static TEST_KEY_2_NSEC: &str = "nsec1ypglg6nj6ep0g2qmyfqcv2al502gje3jvpwye6mthmkvj93tqkesknv6qm"; +pub static TEST_KEY_2_NPUB: &str = + "npub1h2yz2eh0798nh25hvypenrz995nla9dktfuk565ljf3ghnkhdljsul834e"; + +pub static TEST_KEY_2_DISPLAY_NAME: &str = "carole"; +pub static TEST_KEY_2_ENCRYPTED: &str = "...2"; +pub static TEST_KEY_2_KEYS: Lazy = + Lazy::new(|| nostr::Keys::from_sk_str(TEST_KEY_2_NSEC).unwrap()); + +pub static TEST_INVALID_NSEC: &str = "nsec1ppsg5sm2aex"; +pub static TEST_PASSWORD: &str = "769dfd£pwega8SHGv3!#Bsfd5t"; +pub static TEST_INVALID_PASSWORD: &str = "INVALID769dfd£pwega8SHGv3!"; +pub static TEST_WEAK_PASSWORD: &str = "fhaiuhfwe"; +pub static TEST_RANDOM_TOKEN: &str = "lkjh2398HLKJ43hrweiJ6FaPfdssgtrg"; /// wrapper for a cli testing tool - currently wraps rexpect and dialoguer /// @@ -41,6 +65,16 @@ impl CliTester { i.prompt(true).context("initial input prompt")?; Ok(i) } + + pub fn expect_password(&mut self, prompt: &str) -> Result { + let mut i = CliTesterPasswordPrompt { + tester: self, + prompt: prompt.to_string(), + confirmation_prompt: "".to_string(), + }; + i.prompt().context("initial password prompt")?; + Ok(i) + } } pub struct CliTesterInputPrompt<'a> { @@ -101,6 +135,70 @@ impl CliTesterInputPrompt<'_> { } } +pub struct CliTesterPasswordPrompt<'a> { + tester: &'a mut CliTester, + prompt: String, + confirmation_prompt: String, +} + +impl CliTesterPasswordPrompt<'_> { + fn prompt(&mut self) -> Result<&mut Self> { + let p = match self.confirmation_prompt.is_empty() { + true => self.prompt.as_str(), + false => self.confirmation_prompt.as_str(), + }; + + let mut s = String::new(); + self.tester + .formatter + .format_password_prompt(&mut s, p) + .expect("diagluer theme formatter should succeed"); + + ensure!(s.contains(p), "dialoguer must be broken"); + + self.tester + .expect(format!("\r{}", sanatize(s)).as_str()) + .context("expect password input prompt")?; + Ok(self) + } + + pub fn with_confirmation(&mut self, prompt: &str) -> Result<&mut Self> { + self.confirmation_prompt = prompt.to_string(); + Ok(self) + } + + pub fn succeeds_with(&mut self, password: &str) -> Result<&mut Self> { + self.tester.send_line(password)?; + + self.tester + .expect("\r\n") + .context("expect new lines after password input")?; + + if !self.confirmation_prompt.is_empty() { + self.prompt() + .context("expect password confirmation prompt")?; + self.tester.send_line(password)?; + self.tester + .expect("\r\n\r") + .context("expect new lines after password confirmation input")?; + } + + let mut s = String::new(); + self.tester + .formatter + .format_password_prompt_selection(&mut s, self.prompt.as_str()) + .expect("diagluer theme formatter should succeed"); + + ensure!(s.contains(self.prompt.as_str()), "dialoguer must be broken"); + + self.tester + .expect(format!("\r{}\r\n", sanatize(s)).as_str()) + .context("expect password prompt success")?; + + Ok(self) + } +} + impl CliTester { pub fn new(args: I) -> Self where @@ -108,7 +206,17 @@ impl CliTester { S: AsRef, { Self { - rexpect_session: rexpect_with(args).expect("rexpect to spawn new process"), + rexpect_session: rexpect_with(args, 2000).expect("rexpect to spawn new process"), + formatter: ColorfulTheme::default(), + } + } + pub fn new_with_timeout(timeout_ms: u64, args: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + Self { + rexpect_session: rexpect_with(args, timeout_ms).expect("rexpect to spawn new process"), formatter: ColorfulTheme::default(), } } @@ -122,7 +230,7 @@ impl CliTester { .process .exit() .expect("process to exit"); - self.rexpect_session = rexpect_with(args).expect("rexpect to spawn new process"); + self.rexpect_session = rexpect_with(args, 2000).expect("rexpect to spawn new process"); self } @@ -213,7 +321,7 @@ fn sanatize(s: String) -> String { .collect::() } -pub fn rexpect_with(args: I) -> Result +pub fn rexpect_with(args: I, timeout_ms: u64) -> Result where I: IntoIterator, S: AsRef, @@ -224,7 +332,7 @@ where rexpect::session::spawn_with_options( cmd, Options { - timeout_ms: Some(2000), + timeout_ms: Some(timeout_ms), strip_ansi_escape_codes: true, }, ) diff --git a/tests/login.rs b/tests/login.rs index a7e1889..a75608d 100644 --- a/tests/login.rs +++ b/tests/login.rs @@ -3,6 +3,9 @@ use serial_test::serial; use test_utils::*; static EXPECTED_NSEC_PROMPT: &str = "login with nsec (or hex private key)"; +static EXPECTED_SET_PASSWORD_PROMPT: &str = "encrypt with password"; +static EXPECTED_SET_PASSWORD_CONFIRM_PROMPT: &str = "confirm password"; +static EXPECTED_PASSWORD_PROMPT: &str = "password"; fn standard_login() -> Result { let mut p = CliTester::new(["login"]); @@ -10,6 +13,10 @@ fn standard_login() -> Result { p.expect_input_eventually(EXPECTED_NSEC_PROMPT)? .succeeds_with(TEST_KEY_1_NSEC)?; + p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)? + .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? + .succeeds_with(TEST_PASSWORD)?; + p.expect_end_eventually()?; Ok(p) } @@ -19,11 +26,10 @@ mod when_first_time_login { #[test] #[serial] - fn prompts_for_nsec() -> Result<()> { - with_fresh_config(|| { - standard_login()?; - Ok(()) - }) + fn prompts_for_nsec_and_password() -> Result<()> { + before()?; + standard_login()?; + after() } #[test] @@ -35,36 +41,137 @@ mod when_first_time_login { p.expect_input(EXPECTED_NSEC_PROMPT)? .succeeds_with(TEST_KEY_1_NSEC)?; - p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str()) + p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)? + .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? + .succeeds_with(TEST_PASSWORD)?; + + p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) }) } #[test] #[serial] - fn next_time_returns_logged_in_as_npub() -> Result<()> { + fn succeeds_with_hex_secret_key_in_place_of_nsec() -> Result<()> { + with_fresh_config(|| { + let mut p = CliTester::new(["login"]); + + p.expect_input(EXPECTED_NSEC_PROMPT)? + .succeeds_with(TEST_KEY_1_SK_HEX)?; + + p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)? + .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? + .succeeds_with(TEST_PASSWORD)?; + + p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) + }) + } + + mod when_invalid_nsec { + use super::*; + + #[test] + #[serial] + fn prompts_for_nsec_until_valid() -> Result<()> { + with_fresh_config(|| { + let invalid_nsec_response = + "invalid nsec. try again with nsec (or hex private key)"; + + let mut p = CliTester::new(["login"]); + + p.expect_input(EXPECTED_NSEC_PROMPT)? + // this behaviour is intentional. rejecting the response with dialoguer hides + // the original input from the user so they cannot see the + // mistake they made. + .succeeds_with(TEST_INVALID_NSEC)?; + + p.expect_input(invalid_nsec_response)? + .succeeds_with(TEST_INVALID_NSEC)?; + + p.expect_input(invalid_nsec_response)? + .succeeds_with(TEST_KEY_1_NSEC)?; + + p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)? + .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? + .succeeds_with(TEST_PASSWORD)?; + + p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) + }) + } + } +} + +mod when_second_time_login { + use super::*; + + #[test] + #[serial] + fn prints_login_as_npub() -> Result<()> { with_fresh_config(|| { standard_login()?.exit()?; CliTester::new(["login"]) - .expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())? + .expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())? .exit() }) } + + #[test] + #[serial] + fn prompts_for_password_and_succeeds_with_logged_in_as_npub() -> Result<()> { + with_fresh_config(|| { + standard_login()?.exit()?; + + let mut p = CliTester::new(["login"]); + + p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())? + .expect_password(EXPECTED_PASSWORD_PROMPT)? + .succeeds_with(TEST_PASSWORD)?; + + p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) + }) + } + + #[test] + #[serial] + fn when_invalid_password_exit_with_error() -> Result<()> { + with_fresh_config(|| { + standard_login()?.exit()?; + + let mut p = CliTester::new(["login"]); + + p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())? + .expect_password(EXPECTED_PASSWORD_PROMPT)? + .succeeds_with(TEST_INVALID_PASSWORD)?; + p.expect_end_with(format!("Error: failed to log in as {}\r\n\r\nCaused by:\r\n 0: failed to decrypt key with provided password\r\n 1: failed to decrypt\r\n", TEST_KEY_1_NPUB).as_str()) + }) + } } -mod when_called_with_nsec_parameter { +mod when_called_with_nsec_parameter_only { use super::*; #[test] #[serial] fn valid_nsec_param_succeeds_without_prompts() -> Result<()> { with_fresh_config(|| { - CliTester::new(["--nsec", TEST_KEY_2_NSEC, "login"]) - .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())?; + CliTester::new(["login", "--nsec", TEST_KEY_1_NSEC]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) + }) + } + + #[test] + #[serial] + fn forgets_identity() -> Result<()> { + with_fresh_config(|| { + CliTester::new(["login", "--nsec", TEST_KEY_1_NSEC]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; - CliTester::new(["login"]) - .expect(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())? - .exit() + let mut p = CliTester::new(["login"]); + + p.expect_input(EXPECTED_NSEC_PROMPT)? + .succeeds_with(TEST_KEY_1_NSEC)?; + + p.exit() }) } @@ -77,69 +184,212 @@ mod when_called_with_nsec_parameter { with_fresh_config(|| { standard_login()?.exit()?; - CliTester::new(["--nsec", TEST_KEY_2_NSEC, "login"]) - .expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())? - .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())?; - - CliTester::new(["login"]) - .expect(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())? - .exit() + CliTester::new(["login", "--nsec", TEST_KEY_2_NSEC]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str()) }) } } + #[test] + #[serial] + fn invalid_nsec_param_fails_without_prompts() -> Result<()> { + with_fresh_config(|| { + CliTester::new(["login", "--nsec", TEST_INVALID_NSEC]).expect_end_with( + "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n", + ) + }) + } } -mod when_logged_in { +mod when_called_with_nsec_and_password_parameter { use super::*; #[test] #[serial] - fn returns_logged_in_as_npub() -> Result<()> { + fn valid_nsec_param_succeeds_without_prompts() -> Result<()> { with_fresh_config(|| { - standard_login()?.exit()?; + CliTester::new([ + "login", + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + ]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) + }) + } + + #[test] + #[serial] + fn remembers_identity() -> Result<()> { + with_fresh_config(|| { + CliTester::new([ + "login", + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + ]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; CliTester::new(["login"]) - .expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())? + .expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())? .exit() }) } #[test] #[serial] - fn prompts_to_log_in_with_different_nsec() -> Result<()> { + fn parameters_can_be_called_globally() -> Result<()> { with_fresh_config(|| { - standard_login()?.exit()?; - - let mut p = CliTester::new(["login"]); - p.expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())?; - - p.expect_input(EXPECTED_NSEC_PROMPT)? - .succeeds_with(TEST_KEY_2_NSEC)?; - - p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str()) + CliTester::new([ + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + "login", + ]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) }) } + mod when_logging_in_as_different_nsec { use super::*; #[test] #[serial] - fn confirmed_as_logged_in_as_additional_user() -> Result<()> { + fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> { with_fresh_config(|| { standard_login()?.exit()?; - let mut p = CliTester::new(["login"]); - p.expect(format!("logged in as {}\r\n", TEST_KEY_1_NSEC).as_str())?; + CliTester::new([ + "login", + "--nsec", + TEST_KEY_2_NSEC, + "--password", + TEST_PASSWORD, + ]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str()) + }) + } - p.expect_input(EXPECTED_NSEC_PROMPT)? - .succeeds_with(TEST_KEY_2_NSEC)?; + #[test] + #[serial] + fn remembers_identity() -> Result<()> { + with_fresh_config(|| { + standard_login()?.exit()?; - p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())?; + CliTester::new([ + "login", + "--nsec", + TEST_KEY_2_NSEC, + "--password", + TEST_PASSWORD, + ]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str())?; CliTester::new(["login"]) - .expect(format!("logged in as {}\r\n", TEST_KEY_2_NSEC).as_str())? + .expect(format!("login as {}\r\n", TEST_KEY_2_NPUB).as_str())? .exit() }) } } + + mod when_provided_with_new_password { + use super::*; + + #[test] + #[serial] + fn password_changes() -> Result<()> { + with_fresh_config(|| { + standard_login()?.exit()?; + + CliTester::new([ + "login", + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_INVALID_PASSWORD, + ]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; + + CliTester::new(["--password", TEST_INVALID_PASSWORD, "login"]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) + }) + } + } + + #[test] + #[serial] + fn invalid_nsec_param_fails_without_prompts() -> Result<()> { + with_fresh_config(|| { + CliTester::new([ + "login", + "--nsec", + TEST_INVALID_NSEC, + "--password", + TEST_PASSWORD, + ]) + .expect_end_with( + "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n", + ) + }) + } +} + +mod when_called_with_password_parameter_only { + use super::*; + + #[test] + #[serial] + fn when_nsec_stored_logs_in_without_prompts() -> Result<()> { + with_fresh_config(|| { + standard_login()?.exit()?; + + CliTester::new(["login", "--password", TEST_PASSWORD]) + .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) + }) + } + + #[test] + #[serial] + fn when_no_nsec_stored_logs_error() -> Result<()> { + with_fresh_config(|| { + CliTester::new(["login", "--password", TEST_PASSWORD]) + .expect_end_with("Error: no nsec available to decrypt with specified password\r\n") + }) + } +} + +mod when_weak_password { + use super::*; + + #[test] + #[serial] + // combined into a single test as it is computationally expensive to run + fn warns_it_might_take_a_few_seconds_then_succeeds_then_second_login_prompts_for_password_then_warns_again_then_succeeds() + -> Result<()> { + with_fresh_config(|| { + let mut p = CliTester::new_with_timeout(10000, ["login"]); + p.expect_input(EXPECTED_NSEC_PROMPT)? + .succeeds_with(TEST_KEY_1_NSEC)?; + + p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)? + .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? + .succeeds_with(TEST_WEAK_PASSWORD)?; + + p.expect("this may take a few seconds...\r\n")?; + + p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; + + p = CliTester::new_with_timeout(10000, ["login"]); + + p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())? + .expect_password(EXPECTED_PASSWORD_PROMPT)? + .succeeds_with(TEST_WEAK_PASSWORD)?; + + p.expect("this may take a few seconds...\r\n")?; + + p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) + }) + } } -- cgit v1.2.3