From 5494490c40e494c72eb8ed86fb4ed020d7862cca Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Wed, 2 Nov 2022 14:11:07 +0100 Subject: [PATCH] crypto: implement an OpenSSH keystore This will become the primary keystore. Signed-off-by: Alexis Sellier --- Cargo.lock | 485 ++++++++++++++++++++++++++++- radicle-crypto/Cargo.toml | 12 +- radicle-crypto/src/ssh.rs | 1 + radicle-crypto/src/ssh/keystore.rs | 108 +++++++ 4 files changed, 597 insertions(+), 9 deletions(-) create mode 100644 radicle-crypto/src/ssh/keystore.rs diff --git a/Cargo.lock b/Cargo.lock index 89f4aa26..ac099405 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -129,12 +140,35 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base64" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3806a8db60cf56efee531616a34a6aaa9a114d6da2add861b0fa4a188881b2c7" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2 0.10.6", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -197,6 +231,16 @@ dependencies = [ "siphasher", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bumpalo" version = "3.11.0" @@ -266,6 +310,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "const-oid" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "722e23542a15cea1f65d4a1419c4cfd7a26706c70871a13a04238ca3f40f1661" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -315,6 +365,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -331,6 +393,28 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + [[package]] name = "data-encoding" version = "2.3.2" @@ -357,6 +441,17 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dd2ae565c0a381dde7fade45fce95984c568bdcb4700a4fdbe3175e0380b2f" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "digest" version = "0.9.0" @@ -373,10 +468,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ "block-buffer 0.10.3", + "const-oid", "crypto-common", "subtle", ] +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + [[package]] name = "ed25519-compact" version = "1.0.15" @@ -384,7 +501,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bee9df587982575886a8682edcee11877894349a805f25629c27f63abe3e9ae8" dependencies = [ "ct-codecs", - "getrandom", + "getrandom 0.2.7", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "digest 0.10.5", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", ] [[package]] @@ -396,6 +546,16 @@ dependencies = [ "instant", ] +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "flate2" version = "1.0.24" @@ -470,6 +630,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.7" @@ -526,6 +697,17 @@ dependencies = [ "url", ] +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.14" @@ -719,6 +901,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] [[package]] name = "lexopt" @@ -744,6 +929,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libm" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" + [[package]] name = "libz-sys" version = "1.1.8" @@ -882,6 +1073,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566d173b2f9406afbc5510a90925d5a2cd80cae4605631f1212303df265de011" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -892,6 +1100,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -899,6 +1118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -949,6 +1169,28 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2 0.10.6", +] + +[[package]] +name = "p384" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2 0.10.6", +] + [[package]] name = "pbkdf2" version = "0.11.0" @@ -958,6 +1200,15 @@ dependencies = [ "digest 0.10.5", ] +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -996,6 +1247,28 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der", + "pkcs8", + "spki", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.25" @@ -1011,6 +1284,12 @@ dependencies = [ "libc", ] +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1050,7 +1329,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -1119,8 +1398,9 @@ dependencies = [ "serde", "sha2 0.10.6", "sqlite", + "ssh-key", + "tempfile", "thiserror", - "zeroize", ] [[package]] @@ -1220,13 +1500,56 @@ dependencies = [ "radicle", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -1235,7 +1558,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.7", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -1280,6 +1612,38 @@ dependencies = [ "winapi", ] +[[package]] +name = "rfc6979" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c86280f057430a52f4861551b092a01b419b8eacefc7c995eacb9dc132fe32" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + +[[package]] +name = "rsa" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ecc3307be66bfb3574577895555bacfb9a37a8d5cd959444b72ff02495c618" +dependencies = [ + "byteorder", + "digest 0.10.5", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "smallvec", + "subtle", + "zeroize", +] + [[package]] name = "ryu" version = "1.0.11" @@ -1307,6 +1671,20 @@ dependencies = [ "sha2 0.10.6", ] +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "serde" version = "1.0.145" @@ -1397,6 +1775,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.5", + "rand_core 0.6.4", +] + [[package]] name = "siphasher" version = "0.3.10" @@ -1428,6 +1816,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "sqlite" version = "0.27.0" @@ -1458,6 +1862,38 @@ dependencies = [ "sqlite3-src", ] +[[package]] +name = "ssh-encoding" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19cfdc32e0199062113edf41f344fbf784b8205a94600233c84eb838f45191e1" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2 0.10.6", +] + +[[package]] +name = "ssh-key" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288d8f5562af5a3be4bda308dd374b2c807b940ac370b5efa1c99311da91d9a1" +dependencies = [ + "aes", + "bcrypt-pbkdf", + "ctr", + "ed25519-dalek", + "p256", + "p384", + "rand_core 0.6.4", + "rsa", + "sec1", + "sha2 0.10.6", + "signature", + "ssh-encoding", + "zeroize", +] + [[package]] name = "subtle" version = "2.4.1" @@ -1481,6 +1917,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -1760,6 +2208,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unsigned-varint" version = "0.5.1" @@ -1805,6 +2259,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -1941,3 +2401,18 @@ name = "zeroize" version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/radicle-crypto/Cargo.toml b/radicle-crypto/Cargo.toml index 7aef8a24..ff5a9ab3 100644 --- a/radicle-crypto/Cargo.toml +++ b/radicle-crypto/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" [features] test = ["fastrand", "quickcheck"] -ssh = ["base64", "radicle-ssh", "sha2"] +ssh = ["base64", "radicle-ssh", "sha2", "ssh-key"] [dependencies] ed25519-compact = { version = "1.0.12", features = ["pem"] } @@ -28,6 +28,12 @@ optional = true version = "0.1" optional = true +[dependencies.ssh-key] +version = "0.5.1" +default-features = false +features = ["std", "encryption", "rand_core", "getrandom"] +optional = true + [dependencies.quickcheck] version = "1" default-features = false @@ -47,9 +53,7 @@ optional = true version = "0.13" optional = true - [dev-dependencies] -fastrand = { version = "1.8.0" } quickcheck_macros = { version = "1", default-features = false } quickcheck = { version = "1", default-features = false } -zeroize = { version = "1.5.7" } +tempfile = { version = "3.3.0" } diff --git a/radicle-crypto/src/ssh.rs b/radicle-crypto/src/ssh.rs index b06c9a6f..9b969e4f 100644 --- a/radicle-crypto/src/ssh.rs +++ b/radicle-crypto/src/ssh.rs @@ -1,4 +1,5 @@ pub mod agent; +pub mod keystore; use std::io; diff --git a/radicle-crypto/src/ssh/keystore.rs b/radicle-crypto/src/ssh/keystore.rs new file mode 100644 index 00000000..4d7e8167 --- /dev/null +++ b/radicle-crypto/src/ssh/keystore.rs @@ -0,0 +1,108 @@ +use std::path::{Path, PathBuf}; + +use thiserror::Error; + +use crate::{KeyPair, PublicKey, SecretKey}; + +#[derive(Debug, Error)] +pub enum Error { + #[error("ssh keygen: {0}")] + Ssh(#[from] ssh_key::Error), + #[error("invalid key type, expected ed25519 key")] + InvalidKeyType, + #[error("keystore already initialized")] + AlreadyInitialized, +} + +/// Stores keys on disk, in OpenSSH format. +#[derive(Debug)] +pub struct Keystore { + path: PathBuf, +} + +impl Keystore { + /// Create a new keystore pointing to the given path. Use [`Keystore::init`] to initialize. + pub fn new>(path: &P) -> Self { + Self { + path: path.as_ref().to_path_buf(), + } + } + + /// Initialize a keystore by generate a key pair and storing the secret and public keys + /// at the given path. + /// + /// The comment is associated with the private key. + /// The passphrase is used to encrypt the private key. + /// + pub fn init(self, comment: &str, passphrase: &str) -> Result { + let pair = KeyPair::generate(); + let pair = ssh_key::private::Ed25519Keypair::from_bytes(&*pair)?; + let pair = ssh_key::private::KeypairData::Ed25519(pair); + let secret = ssh_key::PrivateKey::new(pair, comment)?; + let secret = secret.encrypt(ssh_key::rand_core::OsRng, passphrase)?; + let public = secret.public_key(); + let path = self.path.join("radicle"); + + if path.exists() { + return Err(Error::AlreadyInitialized); + } + + secret.write_openssh_file(&path, ssh_key::LineEnding::default())?; + public.write_openssh_file(&path.with_extension("pub"))?; + + Ok(self) + } + + /// Load the public key from the store. Returns `None` if it wasn't found. + pub fn public_key(&self) -> Result, Error> { + let path = self.path.join("radicle.pub"); + if !path.exists() { + return Ok(None); + } + + let public = ssh_key::PublicKey::read_openssh_file(&path)?; + match public.key_data() { + ssh_key::public::KeyData::Ed25519(ssh_key::public::Ed25519PublicKey(data)) => { + Ok(Some(PublicKey::from(*data))) + } + _ => Err(Error::InvalidKeyType), + } + } + + /// Load the secret key from the store, decrypting it with the given passphrase. + /// Returns `None` if it wasn't found. + pub fn secret_key(&self, passphrase: &str) -> Result, Error> { + let path = self.path.join("radicle"); + if !path.exists() { + return Ok(None); + } + + let encrypted = ssh_key::PrivateKey::read_openssh_file(&path)?; + let secret = encrypted.decrypt(passphrase)?; + + match secret.key_data() { + ssh_key::private::KeypairData::Ed25519(pair) => { + Ok(Some(SecretKey::new(pair.to_bytes()))) + } + _ => Err(Error::InvalidKeyType), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_init() { + let tmp = tempfile::tempdir().unwrap(); + let store = Keystore::new(&tmp.path()); + + let store = store.init("test", "hunter").unwrap(); + let public = store.public_key().unwrap().unwrap(); + let secret = store.secret_key("hunter").unwrap().unwrap(); + + assert_eq!(PublicKey::from(secret.public_key()), public); + store.secret_key("blunder").unwrap_err(); // Wrong passphrase. + } +}