From 6e811c85c28a13931a0b3f8c0001e9ca1e0bbec9 Mon Sep 17 00:00:00 2001 From: C0ffeeCode Date: Wed, 2 Apr 2025 22:42:20 +0200 Subject: [PATCH] Sealing: Encryption of Secrets (#1) This adds support for encrypting and decrypting secrets. It implements the APIs required for unsealing. The APIs are not complete or compliant. Reviewed-on: https://git.coffeeco.dev/C0ffeeCode/rvault/pulls/1 Squashed commit of the following: commit d77237aefe7693f1772e121f4f94f61eee256ceb Author: C0ffeeCode Date: Wed Apr 2 18:59:33 2025 +0200 Refactor: Secret struct and feature-gates - Shamir and its dependencies behind a default feature - Secret has its own struct commit 6eb02c8412b6b1efc32dfb840e2ac027c4c3b1d4 Author: C0ffeeCode Date: Wed Apr 2 08:28:28 2025 +0200 Feat (sealing): Shamir Secret Sharing scheme commit 5de9e1d74ec69f51efbe363455f34ea901f4d284 Author: C0ffeeCode Date: Thu Mar 27 22:13:57 2025 +0100 Fix (sealing): Simple sealing with random nonce commit 88ed714e22590fb3aaa5a001ec793cb215830d00 Author: C0ffeeCode Date: Thu Mar 27 17:13:48 2025 +0100 Feat (sealing): Simple Password sealing Password is generated on first startup. The password given to the user is not same as the one used to encrypt secrets commit 4d342e8b99a6de6187e834e5a7854fadc55677a8 Author: C0ffeeCode Date: Wed Mar 26 21:51:27 2025 +0100 Feat (kv2): Support Sealing commit 1accd45648ccde7953540a250a241a1918945fed Author: C0ffeeCode Date: Wed Mar 26 21:49:59 2025 +0100 WIP feat (sealing): Implement basic sealing functionality Currently, the key is just stored plainly in the database commit 7949d64649b74c7b389a64a47bd5a02e8b1aad84 Author: C0ffeeCode Date: Wed Mar 26 21:39:07 2025 +0100 Chore: Rename `DatabaseDriver` to `DbPool` and add a custom serde serializer `serialize_reject_none` as a utility --- .gitignore | 5 +- Cargo.lock | 546 ++++++++++++++++++++++--- Cargo.toml | 29 +- go_tests/tests/secret_test.go | 30 +- migrations/20240501152243_KvSecret.sql | 4 +- migrations/20250326160659_sealing.sql | 8 + src/auth.rs | 4 +- src/common.rs | 11 + src/engines.rs | 8 +- src/engines/kv.rs | 5 +- src/engines/kv/data.rs | 91 +++-- src/engines/kv/meta.rs | 4 +- src/engines/kv/post_secret.sql | 9 +- src/engines/kv/structs.rs | 2 - src/identity.rs | 4 +- src/main.rs | 14 +- src/storage.rs | 9 +- src/storage/sealing.rs | 334 +++++++++++++++ src/storage/sealing/shamir.rs | 223 ++++++++++ src/storage/sealing/simple.rs | 47 +++ src/storage/sled.rs | 167 -------- src/sys.rs | 16 +- src/sys/root_generation.rs | 12 + src/sys/sealing.rs | 50 +++ 24 files changed, 1315 insertions(+), 317 deletions(-) create mode 100644 migrations/20250326160659_sealing.sql create mode 100644 src/storage/sealing.rs create mode 100644 src/storage/sealing/shamir.rs create mode 100644 src/storage/sealing/simple.rs delete mode 100644 src/storage/sled.rs create mode 100644 src/sys/root_generation.rs create mode 100644 src/sys/sealing.rs diff --git a/.gitignore b/.gitignore index 8cac0ad..57cc0e4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ *.pdf target/ go_client/openapi.json -crates/storage-sled/sled_db -test.db -src/storage/database.db + +*.db* diff --git a/Cargo.lock b/Cargo.lock index c5c28d4..c23661e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,54 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array 0.14.7", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -99,9 +147,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" dependencies = [ "axum-core", "bytes", @@ -133,12 +181,12 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", @@ -166,6 +214,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -187,13 +241,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -223,6 +289,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -283,16 +359,39 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.7", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "der" version = "0.7.9" @@ -343,6 +442,20 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "either" version = "1.15.0" @@ -352,6 +465,41 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array 0.14.7", + "group", + "hkdf", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "elliptic-curve-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48843edfbd0a370b3dd14cdbb4e446e9a8855311e6b2b57bf9a1fd1367bc317" +dependencies = [ + "elliptic-curve", + "heapless", + "hex", + "multiexp", + "serde", + "zeroize", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -419,6 +567,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + [[package]] name = "flume" version = "0.11.1" @@ -451,6 +610,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -531,6 +696,16 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", +] + +[[package]] +name = "generic-array" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c8444bc9d71b935156cc0ccab7f622180808af7867b1daae6547d773591703" +dependencies = [ + "typenum", ] [[package]] @@ -562,6 +737,36 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -579,7 +784,17 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", ] [[package]] @@ -743,9 +958,9 @@ dependencies = [ [[package]] name = "icu_locid_transform_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" @@ -767,9 +982,9 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" @@ -788,9 +1003,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" @@ -848,7 +1063,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", ] [[package]] @@ -888,25 +1112,12 @@ dependencies = [ ] [[package]] -name = "json-patch" -version = "4.0.0" +name = "keccak" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159294d661a039f7644cea7e4d844e6b25aaf71c1ffe9d73a96d768c24b0faf4" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" -dependencies = [ - "serde", - "serde_json", + "cpufeatures", ] [[package]] @@ -1017,6 +1228,44 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "multiexp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a383da1ae933078ddb1e4141f1dd617b512b4183779d6977e6451b0e644806" +dependencies = [ + "ff", + "group", + "rustversion", + "std-shims", + "zeroize", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "rand", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1034,6 +1283,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1060,6 +1319,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1081,9 +1351,27 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] [[package]] name = "parking" @@ -1168,6 +1456,18 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.0" @@ -1195,7 +1495,16 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.24", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", ] [[package]] @@ -1222,6 +1531,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1290,6 +1605,16 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -1374,9 +1699,9 @@ checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" -version = "0.103.0" +version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", @@ -1393,17 +1718,20 @@ checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" name = "rvault-server" version = "0.1.0" dependencies = [ + "aes-gcm-siv", "axum", + "base64", "dotenvy", "env_logger", - "json-patch", "log", + "p256", "serde", "serde_json", "sqlx", "time", "tokio", "tower", + "vsss-rs", "zeroize", ] @@ -1419,6 +1747,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.7", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "serde" version = "1.0.219" @@ -1473,6 +1815,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1495,6 +1847,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1595,7 +1957,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown", + "hashbrown 0.15.2", "hashlink", "indexmap", "log", @@ -1608,7 +1970,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.12", + "thiserror", "time", "tokio", "tokio-stream", @@ -1675,7 +2037,7 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "generic-array", + "generic-array 0.14.7", "hex", "hkdf", "hmac", @@ -1693,7 +2055,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror", "time", "tracing", "whoami", @@ -1731,7 +2093,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror", "time", "tracing", "whoami", @@ -1767,6 +2129,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "std-shims" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e49360f31b0b75a6a82a5205c6103ea07a79a60808d44f5cc879d303337926" +dependencies = [ + "hashbrown 0.14.5", + "spin", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -1812,6 +2184,12 @@ dependencies = [ "syn", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.19.1" @@ -1825,33 +2203,13 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -2054,6 +2412,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[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.9.0" @@ -2101,6 +2469,25 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsss-rs" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec4ebcc5594130c31b49594d55c0583fe80621f252f570b222ca4845cafd3cf" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "elliptic-curve-tools", + "generic-array 1.2.0", + "hex", + "num", + "rand_core", + "serde", + "sha3", + "subtle", + "zeroize", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2310,6 +2697,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yoke" version = "0.7.5" @@ -2334,13 +2730,33 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + [[package]] name = "zerocopy" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.24", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bed1b56..107a780 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,19 +3,24 @@ name = "rvault-server" version = "0.1.0" edition = "2024" +[features] +default = ["shamir"] +# default = ["insecure-dev-sealing"] +insecure-dev-sealing = [] +shamir = ["vsss-rs", "p256"] + [dependencies] -log = "0.4.21" -env_logger = "0.11.3" -zeroize = { version = "1.7.0", features = ["zeroize_derive"] } -time = { version = "0.3.39", features = ["serde", "formatting"]} -tokio = { version = "1.37.0", features = ["full"] } +log = "0.4.27" +env_logger = "0.11.7" +zeroize = { version = "1.8.1", features = ["zeroize_derive"] } +time = { version = "0.3.41", features = ["serde", "formatting"]} +tokio = { version = "1.44.1", features = ["full"] } tower = { version = "0.5.2", features = [] } -axum = "0.8.1" -serde = "1.0.201" -serde_json = "1.0.117" -json-patch = "4.0.0" -# serde_with = "3.8.1" +axum = "0.8.3" +serde = "1.0.219" +serde_json = "1.0.140" dotenvy = "0.15.7" +base64 = "0.22.1" # utoipa = { version = "4.2.0", features = ["axum_extras"] } sqlx = { version = "0.8.3", features = [ @@ -28,6 +33,10 @@ sqlx = { version = "0.8.3", features = [ "time" ] } +aes-gcm-siv = "0.11.1" +vsss-rs = { version = "5.1.0", optional = true, default-features = false, features = ["zeroize", "std"] } +p256 = { version = "0.13.2", optional = true, default-features = false, features = ["std", "ecdsa"] } + [lints] workspace = true diff --git a/go_tests/tests/secret_test.go b/go_tests/tests/secret_test.go index 9b6de21..b8c8890 100644 --- a/go_tests/tests/secret_test.go +++ b/go_tests/tests/secret_test.go @@ -14,7 +14,7 @@ import ( // vault "github.com/openbao/openbao/api/v2" ) -var client *vault.Client +var Client *vault.Client var ctx context.Context func TestMain(m *testing.M) { @@ -23,35 +23,49 @@ func TestMain(m *testing.M) { config := vault.DefaultConfig() config.Address = "http://localhost:8200" - config.Timeout = 30*time.Second + config.Timeout = 30 * time.Second // prepare a client with the given base address - client, err = vault.NewClient(config) + Client, err = vault.NewClient(config) if err != nil { log.Fatalf("unable to initialize Vault client: %v", err) } log.Println("client prepared") // authenticate with a root token (insecure) - client.SetToken("my-token") + Client.SetToken("my-token") exitCode := m.Run() // run all tests and get code os.Exit(exitCode) } +// Requires in-code portions +// func TestUnseal(t *testing.T) { +// abc := []string{ +// "eyJpIjpbMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwxXSwidiI6WzE4OCw2NiwxMTksMTQ0LDE1OSw3MCw4NiwxMTUsMTIwLDI1MywxMjQsOTYsMTM5LDk0LDQ1LDE2NiwyMTMsMzYsMTE1LDU4LDg5LDE0OCw2MCwyOCwxNTAsMTE2LDU3LDg5LDIwMCw5NywxNDYsMjEzXX0=", +// "eyJpIjpbMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwwLDAsMCwyXSwidiI6WzE1OCwyNDQsNzEsOTUsMTIyLDEzOCwyNDEsMjEzLDQ1LDE1NiwxMTgsNCwxNzYsNiwxNTcsMTkyLDE2MSwxNjEsNDMsMTc1LDE5NSw4NywxODAsMTAwLDE1NiwxNCwxNDgsMTUsMTc4LDkwLDY3LDExOF19", +// } +// for i := range abc { +// if _, err := Client.Sys().Unseal(abc[i]); err != nil { +// t.Fatal("Error unsealing", err) +// } + +// } +// } + func kv2Write(t *testing.T, mount string, path string) { data := map[string]any{ "password1": "123abc", "password2": "horse horse horse battery staple correct", } t.Logf("Attempting to write to KV2 %s path %s:\t", mount, path) - v, err := client.KVv2(mount).Put(ctx, path, data) + v, err := Client.KVv2(mount).Put(ctx, path, data) if err != nil { t.Fatal("ERROR writing secret:\n\t", err) } t.Log("Success (unchecked)\n\t", v) - res, err := client.KVv2(mount).Get(ctx, path) + res, err := Client.KVv2(mount).Get(ctx, path) if err != nil { t.Fatal("ERROR checking/reading secret (request failed)\n\t", err) } @@ -65,11 +79,11 @@ func kv2Write(t *testing.T, mount string, path string) { } func kv2Delete(t *testing.T, mount string, path string) { - err := client.KVv2(mount).Delete(ctx, path) // currently disregarding modifier options + err := Client.KVv2(mount).Delete(ctx, path) // currently disregarding modifier options if err != nil { log.Fatal("ERROR deleting secret:\n\t", err) } - res, err := client.KVv2(mount).Get(ctx, path) + res, err := Client.KVv2(mount).Get(ctx, path) if res != nil || err == nil { t.Fatal("ERROR checking/reading secret (request failed)\n\t", res, err) } diff --git a/migrations/20240501152243_KvSecret.sql b/migrations/20240501152243_KvSecret.sql index 6736fd0..60e8bc6 100644 --- a/migrations/20240501152243_KvSecret.sql +++ b/migrations/20240501152243_KvSecret.sql @@ -21,10 +21,12 @@ CREATE TABLE kv2_secret_version ( secret_path TEXT NOT NULL, version_number INTEGER NOT NULL CHECK ( version_number > 0 ), - secret_data TEXT NOT NULL, created_time DATETIME NOT NULL, deletion_time DATETIME, + encrypted_data BLOB NOT NULL, + nonce BLOB NOT NULL CHECK ( length(nonce) = 12 ), + PRIMARY KEY (engine_path, secret_path, version_number), FOREIGN KEY (engine_path, secret_path) REFERENCES kv2_metadata(engine_path, secret_path) ); diff --git a/migrations/20250326160659_sealing.sql b/migrations/20250326160659_sealing.sql new file mode 100644 index 0000000..772d12a --- /dev/null +++ b/migrations/20250326160659_sealing.sql @@ -0,0 +1,8 @@ +-- Sealing Key + +CREATE TABLE root_key ( + version INTEGER PRIMARY KEY CHECK ( version = 1 ), + encrypted_key BLOB NOT NULL, + nonce BLOB, + type TEXT NOT NULL CHECK ( type IN ('dev_only', 'simple', 'shamir') ) +); diff --git a/src/auth.rs b/src/auth.rs index d6e033d..f1767e3 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,13 +1,13 @@ use axum::Router; -use crate::storage::DatabaseDriver; +use crate::storage::DbPool; // route prefix: `/auth/token/` // mod token; // use self::token::token_auth_router; -pub fn auth_router(pool: DatabaseDriver) -> Router { +pub fn auth_router(pool: DbPool) -> Router { Router::new().with_state(pool) // .nest("/token", token_auth_router()) } diff --git a/src/common.rs b/src/common.rs index f3a0cee..928ad2c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -21,3 +21,14 @@ impl HttpError { HttpError::new(status_code, vec![error.to_string(); 1]) } } + +/// Custom serialization function for `secret_data` +pub fn serialize_reject_none(value: &Option, serializer: S) -> Result +where + S: serde::Serializer, +{ + match value { + Some(data) => serializer.serialize_str(data), + None => Err(serde::ser::Error::custom("`secret_data` must not be None during serialization!")), + } +} diff --git a/src/engines.rs b/src/engines.rs index e8f8698..f767c7a 100644 --- a/src/engines.rs +++ b/src/engines.rs @@ -10,13 +10,13 @@ use axum::{ use log::*; use tower::Service; -use crate::{common::HttpError, storage::DatabaseDriver}; +use crate::{common::HttpError, storage::DbPool}; #[derive(Clone)] /// State to be used to store the database pool /// and the routers for each engine struct EngineMapperState { - pool: DatabaseDriver, + pool: DbPool, kv_v2: Router, } @@ -24,7 +24,7 @@ struct EngineMapperState { struct EnginePath(String); /// Secret engine router -pub fn secrets_router(pool: DatabaseDriver) -> Router { +pub fn secrets_router(pool: DbPool) -> Router { // State containing the pool and engine routers let state = EngineMapperState { pool: pool.clone(), @@ -81,7 +81,7 @@ fn unknown_engine(engine_type: String) -> impl IntoResponse { /// Returns the mount path and engine type for the request, /// if the mount path is registed at the database -async fn map_mount_points(req: &Uri, pool: &DatabaseDriver) -> Option<(String, String)> { +async fn map_mount_points(req: &Uri, pool: &DbPool) -> Option<(String, String)> { let mut mount_path_fragments: Vec<&str> = req.path().split('/').collect(); // Find longest matching existing mount path for the request diff --git a/src/engines/kv.rs b/src/engines/kv.rs index 18aff42..2d2281e 100644 --- a/src/engines/kv.rs +++ b/src/engines/kv.rs @@ -5,14 +5,13 @@ mod meta; // #[cfg(test)] // mod tests; -use crate::storage::DatabaseDriver; +use crate::storage::DbPool; use axum::{ Router, - extract::{Path, State}, routing::*, }; -pub fn kv_router(pool: DatabaseDriver) -> Router { +pub fn kv_router(pool: DbPool) -> Router { Router::new() .route("/config", get(get_config)) .route("/config", post(post_config)) diff --git a/src/engines/kv/data.rs b/src/engines/kv/data.rs index faf0dd5..0e1c0c5 100644 --- a/src/engines/kv/data.rs +++ b/src/engines/kv/data.rs @@ -1,11 +1,8 @@ use super::structs::KvV2WriteRequest; use crate::{ - DatabaseDriver, - common::HttpError, - engines::{ - EnginePath, - kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, - }, + common::HttpError, engines::{ + kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath + }, storage::sealing::Secret, DbPool }; use axum::{ Extension, Json, @@ -13,9 +10,9 @@ use axum::{ http::StatusCode, response::{IntoResponse, NoContent, Response}, }; -use log::{error, info, warn}; +use log::{debug, error, info, warn}; use serde::Deserialize; -use time::UtcDateTime; +use time::{OffsetDateTime, UtcDateTime}; #[derive(Deserialize)] pub struct GetDataQuery { @@ -25,25 +22,51 @@ pub struct GetDataQuery { pub version: u32, } +/// Unluckily needed as `sqlx::query_as!()` does not support FromRow derivations +struct SecretDataInternal { + pub created_time: OffsetDateTime, + pub deletion_time: Option, + pub version_number: i64, + pub secret_path: String, + + pub nonce: Vec, + pub encrypted_data: Vec, +} + +impl SecretDataInternal { + pub async fn into_external(self) -> KvSecretData { + let secret = Secret::new(self.encrypted_data, self.nonce).decrypt().await; + KvSecretData { + created_time: self.created_time, + deletion_time: self.deletion_time, + version_number: self.version_number, + secret_path: self.secret_path, + secret_data: secret.unwrap(), + } + } +} + pub async fn get_data( - State(pool): State, + State(pool): State, Query(params): Query, Path(path): Path, Extension(EnginePath(engine_path)): Extension, ) -> Result { - let res = if params.version == 0 { + debug!("Get request: Engine: {engine_path}, path: {path}",); + + let res = if params.version != 0 { // With specific version sqlx::query_as!( - KvSecretData, - r#"SELECT secret_data, created_time, deletion_time, version_number, secret_path + SecretDataInternal, + r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL AND version_number = $3"#, engine_path, path, params.version).fetch_one(&pool).await } else { // Without specific version sqlx::query_as!( - KvSecretData, - r#"SELECT secret_data, created_time, deletion_time, version_number, secret_path + SecretDataInternal, + r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL ORDER BY version_number DESC LIMIT 1"#, engine_path, path).fetch_one(&pool).await @@ -51,8 +74,10 @@ pub async fn get_data( match res { Ok(secret_content) => { + let secret_content = secret_content.into_external().await; + let inner = secret_content.secret_data; let data = Wrapper { - data: serde_json::from_str(&secret_content.secret_data).unwrap(), + data: serde_json::from_str(&inner).unwrap(), }; let return_secret = KvSecretRes { data, @@ -60,34 +85,33 @@ pub async fn get_data( version: Some(secret_content.version_number), }; let return_secret = Json(return_secret); - info!("{:?}", return_secret); + info!("{return_secret:?}"); Ok(return_secret.into_response()) } Err(e) => match e { sqlx::Error::RowNotFound => { - error!("Row not found {:?}", e); + warn!("Secret not found (could be correct behavior) {e:?}"); Ok(HttpError::simple( StatusCode::NOT_FOUND, "Secret not found within kv2 engine", )) } - _ => panic!("{e:?}"), + _ => panic!("Unhandled error: {e:?}"), }, } } pub async fn post_data( - State(pool): State, + State(pool): State, Path(kv_path): Path, Extension(EnginePath(engine_path)): Extension, Json(secret): Json, ) -> Result { - log::debug!( - "Engine: {}, Secret: {}, Content: {}, Version: {:?}, path: {}", + debug!( + "Engine: {}, Secret: {}, Version: {:?}, path: {}", engine_path, kv_path, - secret.data, secret.version, //.unwrap_or(0), kv_path ); @@ -95,9 +119,14 @@ pub async fn post_data( let created_time = time::UtcDateTime::now(); let ts = created_time.unix_timestamp(); + let content = serde_json::to_string(&secret.data).unwrap(); + + let Secret { nonce, protected_data } = Secret::encrypt(&content).await.unwrap(); + let nonce = nonce.as_slice(); + let mut tx = pool.begin().await.unwrap(); - let res_m = sqlx::query!(" + let _ = sqlx::query!(" INSERT INTO kv2_metadata (engine_path, secret_path, cas_required, created_time, max_versions, updated_time) VALUES ($1, $2, 0, $3, 100, $3) ON CONFLICT(engine_path, secret_path) DO NOTHING; @@ -107,7 +136,8 @@ pub async fn post_data( "src/engines/kv/post_secret.sql", engine_path, kv_path, - secret.data, + nonce, + protected_data, ts, secret.version, ) @@ -117,8 +147,6 @@ pub async fn post_data( tx.commit().await.expect("FAILED TO WRITE TX!"); - warn!("test: {res_m:?} {res_r:?} {}", res_r.version_number); - let res = KvV2WriteResponse { created_time: created_time.into(), custom_metadata: None, @@ -134,11 +162,11 @@ pub async fn post_data( // https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#delete-latest-version-of-secret // https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#delete-secret-versions pub async fn delete_data( - State(pool): State, + State(pool): State, Path(path): Path, Extension(EnginePath(engine_path)): Extension, ) -> Result { - log::debug!("Secret: {}, path: {}", path, path); + debug!("Secret: {path}, path: {path}"); let del_time = UtcDateTime::now().unix_timestamp(); @@ -186,7 +214,7 @@ pub async fn delete_data( error!( "Strange - a version to be deleted has been found but could not be found to set deletion.\n\t{e:?}" ); - // Not commited transactions will be aborted upon drop + // Not committed transactions will be aborted upon drop // tx.rollback().await.unwrap(); return Err(HttpError::simple( StatusCode::INTERNAL_SERVER_ERROR, @@ -201,10 +229,8 @@ pub async fn delete_data( Ok(NoContent.into_response()) } - -// Not pub async fn patch_data( - State(pool): State, + State(pool): State, Path(kv_path): Path, Extension(EnginePath(engine_path)): Extension, Json(secret): Json, @@ -212,4 +238,3 @@ pub async fn patch_data( // TODO: implement only application/merge-patch+json todo!("Not implemented") } - diff --git a/src/engines/kv/meta.rs b/src/engines/kv/meta.rs index caf9844..006ebd9 100644 --- a/src/engines/kv/meta.rs +++ b/src/engines/kv/meta.rs @@ -1,4 +1,4 @@ -use crate::storage::DatabaseDriver; +use crate::storage::DbPool; use axum::extract::{Path, State}; pub async fn delete_path() -> &'static str { @@ -14,7 +14,7 @@ pub async fn get_meta() -> &'static str { } pub async fn post_meta( - State(pool): State, + State(pool): State, Path((mount_path, kv_path)): Path<(String, String)>, body: String, ) -> &'static str { diff --git a/src/engines/kv/post_secret.sql b/src/engines/kv/post_secret.sql index 920d7b7..9f44d07 100644 --- a/src/engines/kv/post_secret.sql +++ b/src/engines/kv/post_secret.sql @@ -4,14 +4,15 @@ WITH latest_version AS ( FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 -- engine_path AND secret_path ) -INSERT INTO kv2_secret_version (engine_path, secret_path, secret_data, created_time, version_number) +INSERT INTO kv2_secret_version (engine_path, secret_path, nonce, encrypted_data, created_time, version_number) VALUES ( $1, -- engine_path $2, -- secret_path - $3, -- secret_data - $4, -- created_time + $3, -- nonce + $4, -- encrypted_data + $5, -- created_time CASE -- Use provided version if given - WHEN $5 IS NOT NULL THEN $5 -- version_number (optional) + WHEN $6 IS NOT NULL THEN $6 -- version_number (optional) ELSE COALESCE((SELECT max_version FROM latest_version) + 1, 1) -- otherwise 1 END -- version_number logic ) diff --git a/src/engines/kv/structs.rs b/src/engines/kv/structs.rs index 64af36a..b420ec7 100644 --- a/src/engines/kv/structs.rs +++ b/src/engines/kv/structs.rs @@ -15,7 +15,6 @@ use time::{OffsetDateTime, UtcDateTime, serde::rfc3339}; // } #[derive(Serialize, Deserialize, Debug, Clone)] -/// For SQLite support pub struct KvSecretData { pub secret_data: String, #[serde(with = "rfc3339")] @@ -43,7 +42,6 @@ pub struct Wrapper { pub data: T, } - #[derive(Serialize, Deserialize, Debug)] pub struct KvSecretRes { /// Map (required) diff --git a/src/identity.rs b/src/identity.rs index 3bdce76..128e0e3 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -1,7 +1,7 @@ use axum::Router; -use crate::storage::DatabaseDriver; +use crate::storage::DbPool; -pub fn identity_router(pool: DatabaseDriver) -> Router { +pub fn identity_router(pool: DbPool) -> Router { Router::new().with_state(pool) } diff --git a/src/main.rs b/src/main.rs index 575e296..5cf8d2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,16 @@ +#![forbid(unsafe_code)] + use axum::{ + Router, extract::Request, http::StatusCode, middleware::{self, Next}, response::{IntoResponse, Response}, routing::get, - Router, }; use log::*; use std::{env, net::SocketAddr, str::FromStr}; -use storage::DatabaseDriver; +use storage::DbPool; use tokio::{net::TcpListener, signal}; use crate::common::HttpError; @@ -48,7 +50,11 @@ async fn main() { .layer(middleware::from_fn(set_default_content_type_json)) .with_state(pool.clone()); - warn!("Listening on {}", listen_addr.to_string()); + if !storage::sealing::prepare_unseal(&pool).await { + storage::sealing::init_default(&pool).await; + } + + warn!("Listening on {listen_addr}"); // Start listening let listener = TcpListener::bind(listen_addr).await.unwrap(); axum::serve(listener, app) @@ -70,7 +76,7 @@ async fn set_default_content_type_json( Ok(next.run(req).await) } -async fn shutdown_signal(pool: DatabaseDriver) { +async fn shutdown_signal(pool: DbPool) { let ctrl_c = async { signal::ctrl_c() .await diff --git a/src/storage.rs b/src/storage.rs index 517737e..97cbb25 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,16 +1,19 @@ +pub mod sealing; + use std::{fs::File, path::Path}; use log::*; use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite}; -pub(crate) type DatabaseDriver = Pool; +pub(crate) type DbType = Sqlite; +pub(crate) type DbPool = Pool; -pub async fn create_pool(db_url: String) -> DatabaseDriver { +pub async fn create_pool(db_url: String) -> DbPool { // Create SQLite database file if it does not exist if db_url.starts_with("sqlite:") && db_url != ("sqlite::memory:") { let path = db_url.replace("sqlite:", ""); if !Path::new(&path).exists() { - warn!("Sqlite database does not exist, creating file {}", path); + warn!("Sqlite database does not exist, creating file {path}"); File::create(&path).expect("Failed to create database file"); } } diff --git a/src/storage/sealing.rs b/src/storage/sealing.rs new file mode 100644 index 0000000..8dc9580 --- /dev/null +++ b/src/storage/sealing.rs @@ -0,0 +1,334 @@ +#[cfg(feature = "shamir")] +pub mod shamir; +pub mod simple; + +use aes_gcm_siv::{ + AeadCore, Aes256GcmSiv, KeyInit, + aead::{Aead, OsRng}, +}; +use log::{error, info, warn}; +use simple::SimpleSealing; +use tokio::sync::RwLock; + +use super::DbPool; + +#[derive(PartialEq)] +enum KeyEnum { + /// Final key + MainKey(Vec), + /// Encrypted with single secret (protected_rk, nonce) + Simple(SimpleSealing), + #[cfg(feature = "shamir")] + // Shamir's Secret Sharing + Shamir(shamir::ShamirBucket), + /// Unknown or not initialized + Uninitialized, +} + +trait Sealing { + fn new(protected_rk: Vec, nonce: Vec) -> Self; + + async fn unseal(&mut self, key: String) -> UnsealResult; +} + +struct ProtectedRK { + pub protection_type: String, + pub encrypted_key: Vec, + pub nonce: Option>, +} + +static ROOT_KEY_MAYBE: RwLock = RwLock::const_new(KeyEnum::Uninitialized); + +/// Returns `true` if vault is initialized or unsealed. +/// Returns `false` if uninitialized (nothing in the database). +pub async fn prepare_unseal(pool: &DbPool) -> bool { + { + if !matches!(*ROOT_KEY_MAYBE.read().await, KeyEnum::Uninitialized) { + info!("Vault unseal is already prepared"); + return true; + } + } + + let lock = ROOT_KEY_MAYBE.write(); // Not awaited just here + + let rk = sqlx::query_as!( + ProtectedRK, + "SELECT encrypted_key, type as protection_type, nonce FROM root_key ORDER BY version LIMIT 1" + ) + .fetch_optional(pool) + .await + .expect("Failed to optionally read root key from the database"); + + let v = match rk { + Some(v) => v, + None => { + warn!("No root key was found in the database!"); + return false; + } + }; + info!( + "Root key of type {} found in the database", + v.protection_type + ); + + let mut lock = lock.await; + let nonce = v.nonce.expect("Simple encryption but the nonce is missing"); + let res = match &*v.protection_type { + #[cfg(feature = "insecure-dev-sealing")] + "dev_only" => { + warn!( + "Root key is of type {}. This is INSECURE and must only be used for development purposes!", + v.protection_type + ); + KeyEnum::MainKey(v.encrypted_key) + } + #[cfg(not(feature = "insecure-dev-sealing"))] + "dev_only" => panic!( + r#"Database is insecure but "insecure-dev-sealing" is not enabled for this build!"# + ), + "simple" => KeyEnum::Simple(SimpleSealing::new(v.encrypted_key, nonce)), + #[cfg(feature = "shamir")] + "shamir" => KeyEnum::Shamir(shamir::ShamirBucket::new(v.encrypted_key, nonce)), + #[cfg(not(feature = "shamir"))] + "shamir" => panic!(r#"Feature "shamir" is not enabled for this build!"#), + _ => panic!("Unknown root key type in database"), + }; + *lock = res; + true +} + +/// Must NOT be used in production. +/// Token is plainly stored in the database and will be unsealed directly by [prepare_unseal]! +/// Danger! +#[cfg(feature = "insecure-dev-sealing")] +pub async fn init_insecure_in_db(pool: &DbPool) { + let root_key = Aes256GcmSiv::generate_key(&mut OsRng); + let root_key = root_key.as_slice().to_owned(); + + warn!( + "Danger: INSECURE! Generated root key is stored plainly in the database. Must ONLY be used for development!" + ); + write_new_root_key(pool, root_key, "dev_only", Some(b"")).await; +} + +async fn write_new_root_key( + pool: &DbPool, + protected_key: Vec, + type_to_be: &str, + nonce: Option<&[u8]>, +) { + let _ = sqlx::query!( + " + INSERT INTO root_key (encrypted_key, type, version, nonce) + VALUES ($1, $2, 1, $3) + ", + protected_key, + type_to_be, + nonce + ) + .execute(pool) + .await + .expect("Failed to write new root key to the database"); + + info!("Initialized new root key!"); +} + +pub async fn reseal(pool: &DbPool) { + { + let mut lock = ROOT_KEY_MAYBE.write().await; + *lock = KeyEnum::Uninitialized; + } + prepare_unseal(pool).await; +} + +// pub async fn sealing_status() { +// let lock = ROOT_KEY_MAYBE.read().await; +// match &*lock { +// KeyEnum::MainKey(_) => todo!(), +// KeyEnum::Simple(_, _) => todo!(), +// KeyEnum::Uninitialized => todo!(), +// KeyEnum::Shamir(_, _) => todo!(), +// } +// } + +pub async fn provide_key(key: String) -> UnsealResult { + // First, check if we need to write-lock at all + { + let read_lock = ROOT_KEY_MAYBE.read().await; + if matches!(*read_lock, KeyEnum::MainKey(_)) { + info!("Providing keys is useless since vault is already unlocked"); + return UnsealResult::AlreadyDone; + } else if matches!(*read_lock, KeyEnum::Uninitialized) { + error!("Cannot process provided key when the vault is uninitialized"); + return UnsealResult::Uninitialized; + } + } + + // A write lock is necessary. + let mut write_lock = ROOT_KEY_MAYBE.write().await; + let rk = match &mut *write_lock { + KeyEnum::MainKey(_) | KeyEnum::Uninitialized => { + unreachable!("Should have been checked above") + } + KeyEnum::Simple(simple) => simple.unseal(key).await, + #[cfg(feature = "shamir")] + KeyEnum::Shamir(shamir) => shamir.unseal(key).await, + }; + let rk = match rk { + UnsealResult::DoneConfidential(rk) => rk, + UnsealResult::Done => unreachable!(), + reject_action => return reject_action, + }; + *write_lock = KeyEnum::MainKey(rk); + + info!("Unsealing done; Vault ready"); + UnsealResult::Done +} + +pub struct Secret { + pub nonce: [u8; 12], + pub protected_data: Vec, +} + +impl Secret { + pub fn new(data: D, nonce: N) -> Self + where + D: Into>, + N: AsRef<[u8]>, + { + let nonce_slice = nonce.as_ref(); + assert!( + nonce_slice.len() == 12, + "Nonce must be exactly 12 bytes long" + ); + + let nonce: &[u8; 12] = nonce_slice.try_into().expect("Nonce must be 12 bytes long"); + + Self { + nonce: *nonce, + protected_data: data.into(), + } + } + + /// Encrypt a secret + /// + /// # Panics + /// + /// Panics if . + /// + /// # Errors + /// + /// This function will return an error if the vault is uninitialized or an unknown error occurs. + pub async fn encrypt(data: &String) -> Result { + let cipher = if let KeyEnum::MainKey(key) = &*ROOT_KEY_MAYBE.read().await { + match Aes256GcmSiv::new_from_slice(key) { + Ok(v) => v, + Err(e) => { + error!("Failed to create new AesGcmSiv cipher from variable size key: {e}"); + return Err(()); + } + } + } else { + error!("Cannot encrypt secret since the vault is not unsealed"); + return Err(()); + }; + + let nonce: aes_gcm_siv::aead::generic_array::GenericArray< + u8, + ::NonceSize, + > = Aes256GcmSiv::generate_nonce(&mut OsRng); // 96-bits; unique per message + let enc = match cipher.encrypt(&nonce, data.as_bytes()) { + Ok(v) => v, + Err(e) => { + error!("Failed to encrypt secret with cipher: {e}"); + return Err(()); + } + }; + debug_assert!(nonce.len() == 12, "Nonce should be exactly 12 bytes"); + let nonce = match nonce.as_slice().try_into() { + Ok(v) => v, + Err(e) => { + error!("Nonce should be exactly 12 bytes: {e}"); + return Err(()); + } + }; + Ok(Self { + nonce, + protected_data: enc, + }) + } + + pub async fn decrypt_bytes(self) -> Result, ()> { + assert!(self.nonce.len() == 12); + let cipher = match &*ROOT_KEY_MAYBE.read().await { + KeyEnum::MainKey(key) => Aes256GcmSiv::new_from_slice(key), + _ => panic!("Cannot seal secret since the vault is not unsealed"), + } + .expect("Failed to create new AesGcmSiv cipher from variable size key"); + + let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(&self.nonce); + let enc = match cipher.decrypt(nonce, self.protected_data.as_ref()) { + Ok(v) => v, + Err(e) => { + error!("Failed to decrypt secret with given nonce and cipher: {e}"); + return Err(()); + } + }; + Ok(enc) + } + + pub async fn decrypt(self) -> Result { + String::from_utf8(self.decrypt_bytes().await?).map_err(|e| { + error!("Failed to parse secret as UTF8: {e}"); + }) + } +} + +pub enum UnsealResult { + /// Unsealing finished, with root key hidden + Done, + /// Was already unsealed, no action taken + AlreadyDone, + /// Could not unseal as the vault is uninitialized + Uninitialized, + + /// Unsealing finished, returns root key + DoneConfidential(Vec), + /// Unsealing attempt has been recorded but is not sufficient + Unfinished, + /// The provided or the set of previously provided portions are invalid. + /// Unsealing has been reset. + InvalidReset, + /// Duplicate share + Duplicate, + /// Error processing share, invalid + InvalidRejected, +} + +pub async fn init_default(pool: &DbPool) { + #[cfg(feature = "insecure-dev-sealing")] + let user_key = { + storage::sealing::init_insecure_in_db(&pool).await; + "INSECURE automatic unlock - TESTING ONLY" + }; + + #[cfg(not(feature = "insecure-dev-sealing"))] + let user_key = { + #[cfg(not(feature = "shamir"))] + { + simple::init_simple(&pool).await + } + + #[cfg(feature = "shamir")] + { + shamir::init_shamir(&pool, 2, 5).await + } + }; + + let success = prepare_unseal(&pool).await; + warn!("New sealing password generated: {user_key:?}"); + assert!( + success, + "Vault ought to have been initialized just now but it is not." + ); +} diff --git a/src/storage/sealing/shamir.rs b/src/storage/sealing/shamir.rs new file mode 100644 index 0000000..2a9e513 --- /dev/null +++ b/src/storage/sealing/shamir.rs @@ -0,0 +1,223 @@ +use aes_gcm_siv::{ + AeadCore, Aes256GcmSiv, KeyInit, + aead::{Aead, OsRng, generic_array::GenericArray}, +}; +use base64::{Engine, prelude::BASE64_STANDARD}; +use log::{error, info, warn}; +use p256::{NonZeroScalar, Scalar, SecretKey}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use vsss_rs::{ + DefaultShare, Error as VsssErr, IdentifierPrimeField, ReadableShareSet, ShareElement, + ValuePrimeField, +}; +use zeroize::ZeroizeOnDrop; + +use crate::DbPool; + +use super::{write_new_root_key, Sealing, UnsealResult}; + +type P256Share = DefaultShare, IdentifierPrimeField>; + +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ZeroizeOnDrop)] +/// Differs from [P256Share] by containing Strings +struct ShamirPortion { + #[serde(rename = "i")] + pub identifier: Vec, + #[serde(rename = "v")] + pub value: Vec, +} + +#[derive(PartialEq)] +pub struct ShamirBucket { + portions: Vec, + protected_rk: Vec, + nonce: Vec, +} + +impl Sealing for ShamirBucket { + fn new(protected_rk: Vec, nonce: Vec) -> Self { + Self { + portions: Vec::with_capacity(2), + protected_rk, + nonce, + } + } + + async fn unseal(&mut self, key: String) -> UnsealResult { + let key = match BASE64_STANDARD.decode(key) { + Ok(v) => v, + Err(e) => { + warn!("Portion could not be decoded: {e}"); + return UnsealResult::InvalidRejected; + } + }; + let key_portion: ShamirPortion = match serde_json::from_slice(&key) { + Ok(v) => v, + Err(e) => { + info!("Portion could not be parsed: {e}"); + return UnsealResult::InvalidRejected; + } + }; + + if self.portions.contains(&key_portion) { + warn!("The supplied Shamir portion is already known. Duplication ignored."); + return UnsealResult::Duplicate; + } + self.portions.push(key_portion); + + let abc = match join_keys(&self.portions) { + Ok(v) => v, + Err(e) => { + return match e { + VsssErr::SharingMinThreshold => { + info!("Shamir portion provided. Sharing threshold not reached."); + UnsealResult::Unfinished + }, + VsssErr::SharingDuplicateIdentifier => unreachable!("Addition of duplicate keys should have been prevented by not recording them"), + e => { + error!("Unknown error occurred upon joining keys {e:?}"); + unreachable!() + }, + }; + } + } + .to_bytes(); + + let cipher = match Aes256GcmSiv::new_from_slice(&abc) { + Ok(v) => v, + Err(e) => { + info!("Cipher could not be created from slice: {e}"); + return UnsealResult::InvalidRejected; + } + }; + debug_assert_eq!(self.nonce.len(), 12); + let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(&self.nonce); + let root_key = cipher.decrypt(nonce, self.protected_rk.as_ref()); + match root_key { + Ok(v) => UnsealResult::DoneConfidential(v), + Err(_) => { + // Err is opaque on purpose + self.portions.clear(); + warn!( + "Enough shares have been provided but the set of shares is invalid. The set of shares has been reset." + ); + UnsealResult::InvalidReset + } + } + } +} + +/// Shamir Secret Sharing does not verify a portion for validity, +/// unlike Feldman Verified Secret Sharing, which is built on Shamir. +/// "Validation" happens by attempting to decrypt the root key. +/// +/// # Returns +/// List of encoded key portions +pub async fn init_shamir(pool: &DbPool, threshold: usize, limit: usize) -> Vec { + let root_key = Aes256GcmSiv::generate_key(&mut OsRng); + let nonce: GenericArray::NonceSize> = + Aes256GcmSiv::generate_nonce(&mut OsRng); // 96-bits; unique per message + let root_key = root_key.as_slice().to_owned(); + + let (user_key, protected_rk) = { + let key = Aes256GcmSiv::generate_key(&mut OsRng); + let cipher = Aes256GcmSiv::new(&key); + let nonce: &[u8] = nonce.as_slice(); + debug_assert_eq!(nonce.len(), 12); + let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(nonce); + let enc = cipher.encrypt(nonce, root_key.as_slice()).unwrap(); + (key, enc) + }; + + let portions = share_keys(&mut OsRng, threshold, limit, &user_key); + + log::debug!("Shared Keys: {portions:?}"); + + write_new_root_key(pool, protected_rk, "shamir", Some(nonce.as_slice())).await; + portions +} + +/// Returns a Vec of Base64 encoded JSON-wrapped identifier-value pairs +fn share_keys( + mut osrng: &mut OsRng, + threshold: usize, + limit: usize, + root_key: &[u8], +) -> Vec { + log::debug!("RK: {root_key:?}"); + assert!( + threshold <= limit, + "Threshold cannot be higher than the number of shares (limit)" + ); + + let rk_array = GenericArray::from_slice(root_key); + let rk_scalar = NonZeroScalar::from_repr(*rk_array).unwrap(); + let shared_secret = IdentifierPrimeField(*rk_scalar.as_ref()); + let res = + vsss_rs::shamir::split_secret::(threshold, limit, &shared_secret, &mut osrng); + + res.unwrap() + .iter() + .map(|f| { + BASE64_STANDARD.encode( + json!(ShamirPortion { + identifier: f.identifier.to_vec(), + value: f.value.to_vec(), + }) + .to_string(), + ) + }) + .collect() +} + +fn join_keys(shares: &[ShamirPortion]) -> Result { + let shares: Vec = shares + .iter() + .map(|portion| { + let identifier = IdentifierPrimeField::::from_slice(&portion.identifier) + .map_err(|e| { + info!("Portion could not be converted to IdentifierPrimeField: {e}"); + VsssErr::InvalidShare + })?; + let value = ValuePrimeField::::from_slice(&portion.value).map_err(|e| { + info!("Portion could not be converted to ValuePrimeField: {e}"); + VsssErr::InvalidShare + })?; + Ok(P256Share { identifier, value }) + }) + .collect::>()?; + + let scalar = shares.combine()?; + // A little suboptimal thanks to CtOption + let nzs = match NonZeroScalar::from_repr(scalar.0.into()).into_option() { + Some(v) => v, + None => return Err(VsssErr::InvalidShare), + }; + let sk = SecretKey::from(nzs); + Ok(sk) +} + +#[test] +fn split_and_join() { + let root_key = Aes256GcmSiv::generate_key(&mut OsRng); + let root_key = root_key.as_slice().to_owned(); + let kps = share_keys(&mut OsRng, 2, 5, &root_key); + + let kps: Vec<_> = kps + .iter() + .map(|f| { + let b = BASE64_STANDARD + .decode(f) + .expect("A portion could not be decoded from BASE64"); + serde_json::from_slice(&b).expect("A portion could not be parsed as a key pair") + }) + .collect(); + let k = join_keys(&kps).expect("Error on joining key pairs"); + + assert_eq!( + root_key, + k.to_bytes().as_slice(), + "Original key and re-combined key from shares are not equal" + ); +} diff --git a/src/storage/sealing/simple.rs b/src/storage/sealing/simple.rs new file mode 100644 index 0000000..a832efc --- /dev/null +++ b/src/storage/sealing/simple.rs @@ -0,0 +1,47 @@ +use aes_gcm_siv::{ + AeadCore, Aes256GcmSiv, KeyInit, + aead::{Aead, OsRng, generic_array::GenericArray}, +}; +use base64::{Engine, prelude::BASE64_STANDARD}; + +use crate::DbPool; + +use super::{write_new_root_key, Sealing, UnsealResult}; + +/// Pair of protected root key and nonce +#[derive(PartialEq)] +pub struct SimpleSealing(Vec, Vec); + +impl Sealing for SimpleSealing { + fn new(protected_rk: Vec, nonce: Vec) -> Self { + Self(protected_rk, nonce) + } + + async fn unseal(&mut self, key: String) -> UnsealResult { + let key = BASE64_STANDARD.decode(key).unwrap(); + let cipher = Aes256GcmSiv::new_from_slice(&key).unwrap(); + debug_assert_eq!(self.1.len(), 12); + let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(self.1.as_slice()); + UnsealResult::DoneConfidential(cipher.decrypt(nonce, self.0.as_ref()).unwrap()) + } +} + +#[allow(unused)] +pub async fn init_simple(pool: &DbPool) -> String { + let root_key = Aes256GcmSiv::generate_key(&mut OsRng); + let nonce: GenericArray::NonceSize> = + Aes256GcmSiv::generate_nonce(&mut OsRng); // 96-bits; unique per message + let root_key = root_key.as_slice().to_owned(); + + let (user_key, protected_rk) = { + let key = Aes256GcmSiv::generate_key(&mut OsRng); + let cipher = Aes256GcmSiv::new(&key); + let nonce: &[u8] = nonce.as_slice(); + debug_assert_eq!(nonce.len(), 12); + let nonce = aes_gcm_siv::aead::generic_array::GenericArray::from_slice(nonce); + let enc = cipher.encrypt(nonce, root_key.as_slice()).unwrap(); + (key, enc) + }; + write_new_root_key(pool, protected_rk, "simple", Some(nonce.as_slice())).await; + BASE64_STANDARD.encode(user_key) +} diff --git a/src/storage/sled.rs b/src/storage/sled.rs deleted file mode 100644 index d7108ea..0000000 --- a/src/storage/sled.rs +++ /dev/null @@ -1,167 +0,0 @@ -#[cfg(test)] -mod tests { - use super::*; - use base::create_mock_meta; - #[test] - fn test_update_secret() { - let db: sled::Db = sled::open("sled_db").unwrap(); - update_secret(&db, "foo", TempSecret{version: -99, content: "cool".to_string()}); - } - #[test] - fn test_get_secret() { - let db: sled::Db = sled::open("sled_db").unwrap(); - get_secret(&db, "foo"); - } - #[test] - fn test_delete_secret(){ - let db: sled::Db = sled::open("sled_db").unwrap(); - delete_secret(&db, "foo"); - } - #[test] - fn test_meta(){ - let db: sled::Db = sled::open("sled_db").unwrap(); - println!("writing metadata:"); - update_secret_meta(&db, "metatest", create_mock_meta()); - println!("getting metadata:"); - get_secretmeta(&db, "metatest"); - } -} - -use sled::Db; -use base::{deserialize_metadata_struct, deserialize_secret_struct, serialize_metadata_json, serialize_secret_json, SecretMeta, TempSecret}; - -/// [TODO] Currently no proper versioning -/// inserts a secret. If there was already a secret in the given path, the version is incremented -fn update_secret(db: &Db, path: &str, mut secret: TempSecret) { - match get_secret(db, path) { - Some(old_secret) => { - // case secret found. TODO save it somewhere for versioning - secret.version = old_secret.version + 1; - #[cfg(test)] - print!("something was found. new version {} \n", secret.version) - } - None => { - // case new secret - secret.version = 1; - } - } - // if let secret_json = serialize_secret_json(&secret) { - // let _res = db.insert(path, secret_json); // maybe this can be handled cleaner - match serialize_secret_json(&secret) { - Ok(secret_json) => { - #[cfg(test)] - println!("String: {:?}", secret_json.clone()); - let as_ivec = sled::IVec::from(secret_json.into_bytes()); // maybe outsource this in a fn later - #[cfg(test)] - println!("ivec: {:?}", as_ivec); - match db.insert(path, as_ivec) { - Ok(_) => println!("Secret inserted"), - Err(e) => eprintln!("Failed to insert secret: {}", e), - } - } - Err(e) => eprintln!("Failed to serialize secret: {}", e), - } -} - -// !TODO eliminate redundancy: refactor get and update functions to accept generic types! - -// read and return a secret from the DB -//if there is no secret, return None -fn get_secret(db: &Db, path: &str) -> Option{ - let raw_secret; - match db.get(path) { - Ok(Some(ivec)) => { - raw_secret = ivec; - } - Err(e) => { - eprintln!("Error on retrieving secret: {}", e); - return None; - } - Ok(None) => { - return None; - } - } - // outsource this in a fn later. TODO maybe deal with unwrap - let as_str = String::from_utf8(raw_secret.to_vec()).unwrap(); - match deserialize_secret_struct(&as_str) { - Ok(secret) => { - #[cfg(test)] - println!("got some secret: {:?}", secret); - return Some(secret); - } - Err(e) => { - eprintln!("error on secret deserialization: {}", e); - return None; - } - - } -} - - -// TODO write abstract get_something fn -// https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-metadata -fn get_secretmeta(db: &Db, path: &str) -> Option{ - let raw_metadata; - match db.get(path) { - Ok(Some(ivec)) => { - raw_metadata = ivec; - } - Err(e) => { - eprintln!("Error on retrieving metadata: {}", e); - return None; - } - Ok(None) => { - return None; - } - } - let as_str = String::from_utf8(raw_metadata.to_vec()).unwrap(); - match deserialize_metadata_struct(&as_str) { - Ok(meta) => { - #[cfg(test)] - println!("got some metadata: {:?}", meta); - return Some(meta); - } - Err(e) => { - eprintln!("error on secret deserialization: {}", e); - return None; - } - - } -} - -// currently early version (copied from update_secret) -fn update_secret_meta(db: &Db, path: &str, mut meta: SecretMeta) { - match get_secretmeta(db, path) { - Some(meta) => { - // case secret found. TODO save it somewhere for versioning - #[cfg(test)] - print!("something was found. new version {:?} \n", meta) - } - None => { - } - } - match serialize_metadata_json(&meta) { - Ok(meta_json) => { - #[cfg(test)] - println!("String: {:?}", meta_json.clone()); - let as_ivec = sled::IVec::from(meta_json.into_bytes()); // maybe outsource this in a fn later - #[cfg(test)] - println!("ivec: {:?}", as_ivec); - match db.insert(path, as_ivec) { - Ok(_) => println!("Metadata inserted"), - Err(e) => eprintln!("Failed to insert meta: {}", e), - } - } - Err(e) => eprintln!("Failed to serialize meta: {}", e), - } -} - - -/// TODO soft delete the secret version at path. can be undone with undelete_secret -// https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#delete-latest-version-of-secret -// https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#delete-secret-versions - -/// hard delete secret at path -fn delete_secret(db: &Db, path: &str) { - let rem = db.remove(path); -} \ No newline at end of file diff --git a/src/sys.rs b/src/sys.rs index 3dd38dd..d7b2c73 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -1,8 +1,16 @@ -use axum::Router; +mod root_generation; +mod sealing; -use crate::storage::DatabaseDriver; +use axum::Router; +use root_generation::root_generation; +use sealing::sealing_routes; + +use crate::storage::DbPool; /// System routes -pub fn sys_router(pool: DatabaseDriver) -> Router { - Router::new().with_state(pool) +pub fn sys_router(pool: DbPool) -> Router { + Router::new() + .merge(sealing_routes()) + .merge(root_generation()) + .with_state(pool) } diff --git a/src/sys/root_generation.rs b/src/sys/root_generation.rs new file mode 100644 index 0000000..e1aa99f --- /dev/null +++ b/src/sys/root_generation.rs @@ -0,0 +1,12 @@ +use axum::{Router, routing::post}; + +use crate::DbPool; + +pub fn root_generation() -> Router { + Router::new() + // .route("/generate-root", get(get_root_generation_attempt)) + .route("/generate-root", post(generate_new_root)) + // .route("/generate-root", delete(cancel_generate_root)) +} + +async fn generate_new_root() {} diff --git a/src/sys/sealing.rs b/src/sys/sealing.rs new file mode 100644 index 0000000..e634d11 --- /dev/null +++ b/src/sys/sealing.rs @@ -0,0 +1,50 @@ +use axum::{ + extract::State, routing::{get, post, put}, Json, Router +}; +use log::warn; +use serde::Deserialize; + +use crate::storage::{DbPool, sealing}; + +pub fn sealing_routes() -> Router { + Router::new() + .route("/seal", post(seal_post)) + .route("/seal-status", get(seal_status_get)) + .route("/unseal", post(unseal_post)) + // WTF? Again? Its supposed to be POST but actually a PUT + .route("/unseal", put(unseal_post)) +} + +async fn seal_post(State(pool): State) { + sealing::reseal(&pool).await; +} + +#[derive(Deserialize)] +struct UnsealRequest { + /// Required, unless `reset` is true + pub key: Option, + #[serde(default)] + /// Specifies if previously-provided unseal keys are discarded and the unseal process is reset. + pub reset: bool, + // #[serde(default)] + // /// Used to migrate the seal from shamir to autoseal or autoseal to shamir. Must be provided on all unseal key calls. + // pub migrate: bool, +} + +async fn unseal_post(State(pool): State, Json(req): Json) -> Result<(), ()> { + if req.reset { + warn!("Unsealing progress has been reset on unseal request"); + sealing::reseal(&pool).await; + } + + if let Some(key) = req.key { + sealing::provide_key(key).await; + } else if !req.reset { + // No request key nor reset = bad request + return Err(()); + } + + Ok(()) +} + +async fn seal_status_get(State(pool): State) {}