diff --git a/Cargo.lock b/Cargo.lock index 74c8353..3555e18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,30 +4,30 @@ version = 4 [[package]] name = "aead" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common", - "generic-array", + "crypto-common 0.2.2", + "inout", ] [[package]] name = "aes" -version = "0.8.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" dependencies = [ - "cfg-if", "cipher", - "cpufeatures", + "cpubits", + "cpufeatures 0.3.0", ] [[package]] name = "aes-gcm" -version = "0.10.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" dependencies = [ "aead", "aes", @@ -47,7 +47,7 @@ dependencies = [ "ed25519-bip32", "hex", "hmac", - "rand", + "rand 0.10.2", "secp256k1", "serde", "serde_json", @@ -64,9 +64,9 @@ checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "base64" -version = "0.22.1" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bip39" @@ -75,8 +75,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ "bitcoin_hashes", - "rand", - "rand_core", + "rand 0.8.7", + "rand_core 0.6.4", "serde", "unicode-normalization", "zeroize", @@ -100,6 +100,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cc" version = "1.4.0" @@ -118,14 +127,27 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common", + "block-buffer 0.12.1", + "crypto-common 0.2.2", "inout", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -135,6 +157,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -142,10 +173,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + [[package]] name = "cryptoxide" version = "0.6.2" @@ -154,21 +195,30 @@ checksum = "93f80e26fec88f5ae9450cd5f4e59f5c6421abafc919ea68a3edd521f9580c9b" [[package]] name = "ctr" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ "cipher", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] @@ -209,12 +259,23 @@ dependencies = [ ] [[package]] -name = "ghash" -version = "0.5.1" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.1", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "opaque-debug", "polyval", ] @@ -243,12 +304,21 @@ dependencies = [ ] [[package]] -name = "inout" -version = "0.1.4" +name = "hybrid-array" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ - "generic-array", + "typenum", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", ] [[package]] @@ -269,21 +339,14 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "polyval" -version = "0.6.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", + "cpubits", + "cpufeatures 0.3.0", "universal-hash", ] @@ -314,6 +377,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.7" @@ -322,7 +391,17 @@ checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -332,7 +411,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -341,9 +420,15 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "secp256k1" version = "0.29.1" @@ -412,7 +497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -508,12 +593,12 @@ dependencies = [ [[package]] name = "universal-hash" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common", - "subtle", + "crypto-common 0.2.2", + "ctutils", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1e00d98..ba4462b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,13 @@ name = "alkvault" version = "0.1.0" edition = "2021" +rust-version = "1.85" license = "MIT OR Apache-2.0" description = "Local key vault: BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM encryption for securing provider keys, credentials, and identity material" repository = "https://git.alk.dev/alkdev/alkvault" +readme = "README.md" +keywords = ["vault", "bip39", "ed25519", "slip-0010", "encryption"] +categories = ["cryptography", "authentication"] [lib] name = "alkvault" @@ -16,15 +20,15 @@ secp256k1 = ["dep:secp256k1"] [dependencies] bip39 = { version = "2", features = ["rand", "zeroize"] } ed25519-bip32 = "0.4" -aes-gcm = "0.10" +aes-gcm = "0.11" sha2 = "0.10" serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" zeroize = { version = "1", features = ["derive"] } hmac = "0.12" -rand = "0.8" -base64 = "0.22" +rand = { version = "0.10", default-features = false, features = ["sys_rng"] } +base64 = "0.23" secp256k1 = { version = "0.29", optional = true } [dev-dependencies] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..56f9da5 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,192 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name), or refer to, the Work. + (Note: Derivative Works shall not include works that remain separable from, + or merely link (or bind by name) to the interfaces of, the Work and + Derivative Works thereof.) + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2025-2026 Alk Development + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..9ee173b --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-2026 Alk Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e75d83 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# alkvault + +Local key vault: BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key +derivation, and AES-256-GCM encryption for securing provider keys, +credentials, and identity material. + +`alkvault` is a standalone crate with **zero network dependencies**. It +holds the master seed — the root of trust for all derived keys and +encrypted credentials — and provides the cryptographic primitives and a +runtime API for managing it. Nothing important goes in env vars. + +## What it is + +A local key vault built on the principle that **secrets are derived, not +stored**. From a single BIP39 mnemonic, the vault deterministically +derives Ed25519 identity keys, SSH host keys, and AES-256-GCM encryption +keys on demand. External credentials (API keys, OAuth tokens) that can't +be derived are encrypted with a seed-derived key and stored as +`EncryptedData` blobs. + +The vault is **local-only by construction** — direct method calls on +`VaultServiceHandle`, no actor, no message enum, no wire format, no +remote dispatch. The master seed and derived private keys never cross a +process boundary. + +## Usage + +```rust +use alkvault::derivation::PATHS; +use alkvault::{VaultServiceHandle, KeyType}; + +// Generate a new mnemonic and unlock the vault. +let vault = VaultServiceHandle::new(); +let phrase = vault.unlock_new(24)?; // 24-word BIP39 mnemonic +// Store `phrase` securely — it is the root of trust. + +// Derive an Ed25519 identity keypair at m/74'/0'/0'/0'. +let identity = vault.derive_ed25519(PATHS::IDENTITY)?; +assert_eq!(identity.key_type, KeyType::Ed25519); +assert_eq!(identity.private_key.len(), 32); + +// Encrypt an external credential with a seed-derived AES-256-GCM key. +let plaintext = "sk-proj-abc123xyz789"; +let encrypted = vault.encrypt(plaintext, 2)?; +let decrypted = vault.decrypt(&encrypted)?; +assert_eq!(decrypted, plaintext); + +// Lock the vault — purges the seed and all cached derived keys. +vault.lock(); +# Ok::<(), Box>(()) +``` + +## Features + +| Feature | Default | Description | +|---------|---------|-------------| +| `secp256k1` | off | BIP-0032 secp256k1 HD key derivation for Ethereum signing keys (`m/44'/60'/0'/0/0`) | + +Without `secp256k1`, `derive_ethereum_key` returns +`UnsupportedKeyType`. The `secp256k1` crate is a heavy dependency (C +library for curve operations); feature-gating it keeps the default build +lightweight. + +## Derivation paths + +`alkvault` reserves the `74'` coin type (unallocated per SLIP-0044). + +| Path | Purpose | Key type | +|------|---------|----------| +| `m/74'/0'/0'/0'` | Primary identity keypair | Ed25519 | +| `m/74'/0'/0'/{n}'` | Worker/device identity | Ed25519 | +| `m/74'/0'/1'/0'` | SSH host key | Ed25519 | +| `m/74'/2'/0'/0'` | Encryption key (v2) | AES-256-GCM | +| `m/74'/2'/0'/{n}'` | Encryption key (v{n+2}) | AES-256-GCM | +| `m/44'/60'/0'/0/0` | Ethereum signing key | secp256k1 (feature-gated) | + +Key rotation re-encrypts `EncryptedData` from one version to another via +version-indexed derivation paths — same seed, different keys, no new +mnemonic needed. + +## Security model + +- **Seed never persisted.** The BIP39 mnemonic is entered at startup or + via `unlock` and held only in `Zeroize`-protected RAM. `lock()` purges + the seed and all cached derived keys. +- **Zeroize everything sensitive.** `Mnemonic`, `Seed`, + `ExtendedPrivKey`, `EncryptionKey`, `DerivedKey`, and `CachedKey` all + implement `Zeroize` and `ZeroizeOnDrop`. Secret material does not + linger in freed heap memory. +- **OsRng for nonces.** AES-GCM IVs use the operating system's CSPRNG + (`SysRng`), never a thread-local RNG. IV reuse under the same key is + catastrophic for GCM. +- **`DerivedKey` redaction.** `DerivedKey` serializes `private_key` as + `"[REDACTED]"` in all formats (defense-in-depth for logging accidents) + and rejects redacted payloads on deserialization. `Debug` impls also + redact. +- **Move-only, not `Clone`.** `DerivedKey` and `EncryptionKey` are + move-only — no accidental duplication of secret material. +- **No `unwrap()` outside tests.** Vault operations propagate errors. + A poisoned lock is recovered with `unwrap_or_else(|e| e.into_inner())`, + not panicked. + +## Crate independence + +`alkvault` does **not** depend on any application or networking crate. +It defines its own types (`VaultServiceError`, `DerivedKey`, +`EncryptedData`, etc.) and is usable in contexts where networking doesn't +exist — CLI tools, test harnesses, key-derivation utilities, and future +WASM targets. + +## Documentation + +Architecture documentation lives under [`docs/architecture/`](docs/architecture/): + +- [Mnemonic and key derivation](docs/architecture/mnemonic-derivation.md) — BIP39, SLIP-0010, BIP-0032, derivation paths +- [Encryption](docs/architecture/encryption.md) — AES-256-GCM, `EncryptedData`, key versioning +- [Service](docs/architecture/service.md) — `VaultServiceHandle` lifecycle, cache, error model +- [Protocol](docs/architecture/protocol.md) — `DerivedKey` redaction, `KeyType`, serialization +- [Architecture decisions (ADRs)](docs/architecture/decisions/) — standalone crate, local-only dispatch, HD derivation, key rotation + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. \ No newline at end of file diff --git a/src/encryption.rs b/src/encryption.rs index e5c339c..53355c2 100644 --- a/src/encryption.rs +++ b/src/encryption.rs @@ -40,7 +40,9 @@ use aes_gcm::{ aead::{Aead, KeyInit}, Aes256Gcm, Nonce, }; -use rand::{rngs::OsRng, RngCore}; +use base64::prelude::*; +use rand::rngs::SysRng; +use rand::TryRng; use serde::{Deserialize, Serialize}; use std::fmt; use zeroize::Zeroize; @@ -156,27 +158,29 @@ pub(crate) fn encrypt( plaintext: &str, key: &EncryptionKey, ) -> Result { - let cipher = Aes256Gcm::new_from_slice(key.key_bytes()) - .map_err(|e| EncryptionError::Encryption(format!("invalid key length: {e}")))?; + let cipher = Aes256Gcm::new(key.key_bytes().into()); - // Generate random IV (12 bytes for AES-GCM) using OsRng CSPRNG + // Generate random IV (12 bytes for AES-GCM) using the system CSPRNG let mut iv_bytes = [0u8; 12]; - OsRng.fill_bytes(&mut iv_bytes); - let nonce = Nonce::from_slice(&iv_bytes); + SysRng + .try_fill_bytes(&mut iv_bytes) + .map_err(|e| EncryptionError::Encryption(format!("rng failure: {e}")))?; // TODO(Phase B): Use salt in HKDF-based key derivation let mut salt_bytes = [0u8; 32]; - OsRng.fill_bytes(&mut salt_bytes); + SysRng + .try_fill_bytes(&mut salt_bytes) + .map_err(|e| EncryptionError::Encryption(format!("rng failure: {e}")))?; let ciphertext = cipher - .encrypt(nonce, plaintext.as_bytes()) + .encrypt(&Nonce::from(iv_bytes), plaintext.as_bytes()) .map_err(|e| EncryptionError::Encryption(e.to_string()))?; Ok(EncryptedData { key_version: key.key_version, - salt: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, salt_bytes), - iv: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, iv_bytes), - data: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &ciphertext), + salt: BASE64_STANDARD.encode(salt_bytes), + iv: BASE64_STANDARD.encode(iv_bytes), + data: BASE64_STANDARD.encode(&ciphertext), }) } @@ -194,20 +198,22 @@ pub(crate) fn decrypt( encrypted: &EncryptedData, key: &EncryptionKey, ) -> Result { - let cipher = Aes256Gcm::new_from_slice(key.key_bytes()) - .map_err(|e| EncryptionError::Decryption(format!("invalid key length: {e}")))?; + let cipher = Aes256Gcm::new(key.key_bytes().into()); - let iv_bytes = - base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &encrypted.iv) - .map_err(|e| EncryptionError::Decoding(e.to_string()))?; - let nonce = Nonce::from_slice(&iv_bytes); + let iv_bytes = BASE64_STANDARD + .decode(&encrypted.iv) + .map_err(|e| EncryptionError::Decoding(e.to_string()))?; + let nonce: Nonce<_> = iv_bytes + .as_slice() + .try_into() + .map_err(|_| EncryptionError::Decoding("invalid IV length".to_string()))?; - let ciphertext = - base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &encrypted.data) - .map_err(|e| EncryptionError::Decoding(e.to_string()))?; + let ciphertext = BASE64_STANDARD + .decode(&encrypted.data) + .map_err(|e| EncryptionError::Decoding(e.to_string()))?; let plaintext = cipher - .decrypt(nonce, ciphertext.as_ref()) + .decrypt(&nonce, ciphertext.as_ref()) .map_err(|e| EncryptionError::Decryption(e.to_string()))?; String::from_utf8(plaintext).map_err(|e| EncryptionError::Decryption(e.to_string())) diff --git a/tests/test_vectors.rs b/tests/test_vectors.rs index 0bf4574..bdd6d6d 100644 --- a/tests/test_vectors.rs +++ b/tests/test_vectors.rs @@ -282,21 +282,21 @@ fn test_aes256gcm_known_key_encrypt_decrypt() { 0x1e, 0x1f, ]; - let cipher = Aes256Gcm::new_from_slice(&key_bytes).unwrap(); + let cipher = Aes256Gcm::new((&key_bytes).into()); // Known 12-byte nonce let nonce_bytes: [u8; 12] = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, ]; - let nonce = Nonce::from_slice(&nonce_bytes); + let nonce = Nonce::from(nonce_bytes); let plaintext = b"hello, alkvault!"; // Encrypt with known key and nonce - let ciphertext = cipher.encrypt(nonce, plaintext.as_ref()).unwrap(); + let ciphertext = cipher.encrypt(&nonce, plaintext.as_ref()).unwrap(); // Decrypt with same key and nonce - let decrypted = cipher.decrypt(nonce, ciphertext.as_ref()).unwrap(); + let decrypted = cipher.decrypt(&nonce, ciphertext.as_ref()).unwrap(); assert_eq!( decrypted, plaintext,