From 70061dffe65e97cf1815ded14448f36d3a409e48 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 10 Dec 2023 12:10:53 +0300 Subject: [PATCH] feat: encrypt seed and encode as URI --- Cargo.lock | 353 ++++++++++++++++++++++++++- design/seed.md | 18 ++ kytes/Cargo.toml | 17 +- kytes/src/crypto.rs | 86 ------- kytes/src/crypto/encryption.rs | 33 +++ kytes/src/crypto/keys.rs | 8 + kytes/src/crypto/mod.rs | 9 + kytes/src/{ => crypto}/passphrase.rs | 2 + kytes/src/crypto/seed.rs | 124 ++++++++++ kytes/src/error.rs | 9 + kytes/src/lib.rs | 10 +- kytes/src/main.rs | 66 ++++- 12 files changed, 639 insertions(+), 96 deletions(-) create mode 100644 design/seed.md delete mode 100644 kytes/src/crypto.rs create mode 100644 kytes/src/crypto/encryption.rs create mode 100644 kytes/src/crypto/keys.rs create mode 100644 kytes/src/crypto/mod.rs rename kytes/src/{ => crypto}/passphrase.rs (99%) create mode 100644 kytes/src/crypto/seed.rs create mode 100644 kytes/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 4b693ee..be42305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,66 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a318f1f38d2418400f8209655bfd825785afd25aa30bb7ba6cc792e4596748" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "argon2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -14,6 +74,31 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bessie" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30407622e75e71bbc4f53708a3016c75eb960ed417ec11abeae034bd78e1a196" +dependencies = [ + "blake3", + "rand", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "blake3" version = "1.5.0" @@ -27,6 +112,15 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.5.0" @@ -48,12 +142,98 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "constant_time_eq" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.11" @@ -65,13 +245,22 @@ dependencies = [ "wasi", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "kytes" version = "0.1.0" dependencies = [ - "blake3", + "argon2", + "bessie", "bytes", + "clap", "rand", + "thiserror", "z32", ] @@ -81,12 +270,41 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand" version = "0.8.5" @@ -117,12 +335,145 @@ dependencies = [ "getrandom", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "z32" version = "1.0.2" diff --git a/design/seed.md b/design/seed.md new file mode 100644 index 0000000..931fc58 --- /dev/null +++ b/design/seed.md @@ -0,0 +1,18 @@ +# Seed + +Kytes seed is an encrypted seed encoded as URI as follows: + +``` +kytes:seed: +``` + +The `suffix` is a `z-base32` encoded bytes as follows: + +``` + +``` + +## Version 0 + +For version 0 the encrypted seed is using [`bessie`](https://github.com/oconnor663/bessie/blob/44f9500ebeb0f28efc9689184ff5b1d79e2308e0/design.md) version 0.0.1 + diff --git a/kytes/Cargo.toml b/kytes/Cargo.toml index 2278ab1..88e770f 100644 --- a/kytes/Cargo.toml +++ b/kytes/Cargo.toml @@ -5,10 +5,21 @@ edition = "2021" description = "Soaring in the Cloud, but you pull the strings." license = "MIT" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -blake3 = "1.5.0" bytes = "1.5.0" rand = "0.8.5" z32 = "1.0.2" + +# cli +clap = { version = "4.4.11", optional = true, features = ["derive"] } +argon2 = "0.5.2" +thiserror = "1.0.50" +bessie = "0.0.1" + +[features] +default = ["cli"] +cli = ["clap"] + +[[bin]] +name = "kytes" +required-features = ["cli"] diff --git a/kytes/src/crypto.rs b/kytes/src/crypto.rs deleted file mode 100644 index e9deaa3..0000000 --- a/kytes/src/crypto.rs +++ /dev/null @@ -1,86 +0,0 @@ -use bytes::{BufMut, Bytes, BytesMut}; -use rand::Rng; - -const SEED_FILE_PREFIX: &str = "kytes seed"; -const VERSION: u8 = 0; - -const PASSPHRASE_HASHING_ITERATIONS: i32 = 100_000; - -/// Takes an encrypted seed and format it into a seed file as follows: -/// `kytes encrypted-seed v ` -pub fn format_encrypted_seed_file(encrypted_seed: &[u8; 32]) -> Bytes { - let mut seed_file = BytesMut::with_capacity(SEED_FILE_PREFIX.len() + 33); - seed_file.extend_from_slice(SEED_FILE_PREFIX.as_bytes()); - seed_file.extend_from_slice(b" v"); - seed_file.put_u8(VERSION + 48); - seed_file.extend_from_slice(b" "); - seed_file.extend_from_slice(z32::encode(encrypted_seed).as_bytes()); - - seed_file.freeze() -} - -pub fn encrypted_seed_file_version(seed_file: &Bytes) -> Option { - let version_start = SEED_FILE_PREFIX.len() + 2; - let version_end = version_start + 2; - - seed_file - .get(version_start..version_end) - .map(|version| version[0] - 48_u8) -} - -pub fn encrypt_seed_file() { - unimplemented!() -} - -pub fn generate_seed() -> [u8; 32] { - let mut rng = rand::thread_rng(); - rng.gen() -} - -pub fn seed_encryption_key(passphrase: &str) -> [u8; 32] { - let mut hash: [u8; 32] = blake3::hash(passphrase.as_bytes()).into(); - - for i in 0..PASSPHRASE_HASHING_ITERATIONS { - hash = blake3::hash(&hash).into(); - } - - hash.to_owned() -} - -#[cfg(test)] -mod test { - use std::time::Instant; - - use crate::passphrase::generate_4words_passphrase; - - use super::*; - - #[test] - fn test_format_encrypted_seed_file() { - let seed = generate_seed(); - let seed_file = format_encrypted_seed_file(&seed); - - dbg!(&seed_file); - - assert_eq!(seed_file.len(), 52 + 4 + SEED_FILE_PREFIX.len()); - assert!(seed_file.starts_with(SEED_FILE_PREFIX.as_bytes())); - assert!(seed_file.starts_with(SEED_FILE_PREFIX.as_bytes())); - assert_eq!(encrypted_seed_file_version(&seed_file).unwrap(), 0); - assert!(seed_file.ends_with(&z32::encode(&seed).as_bytes())); - } - - #[test] - fn hash() { - let passphrase = generate_4words_passphrase(); - - let start = Instant::now(); - - println!("start hashing..."); - - let hash = seed_encryption_key(&passphrase); - - println!("final hash: {:?}", hash); - - println!("{} ms", start.elapsed().as_millis()); - } -} diff --git a/kytes/src/crypto/encryption.rs b/kytes/src/crypto/encryption.rs new file mode 100644 index 0000000..aced92c --- /dev/null +++ b/kytes/src/crypto/encryption.rs @@ -0,0 +1,33 @@ +//! Encryption functions. + +use crate::{Error, Result}; +use rand::{Rng, RngCore}; + +use crate::crypto::{Key, Nonce}; + +/// Compute the length of a ciphertext, given the length of a plaintext. +/// +/// This function returns `None` if the resulting ciphertext length would overflow a `u64`. +pub fn ciphertext_len(plaintext_len: u64) -> Option { + bessie::ciphertext_len(plaintext_len) +} + +/// Encrypt a message and write the ciphertext to an existing slice. +/// +/// This function does not allocate memory. However, `ciphertext.len()` must be exactly equal to +/// [`ciphertext_len(plaintext.len())`](ciphertext_len), or else this function will panic. +pub fn encrypt_to_slice(key: &Key, plaintext: &[u8], ciphertext: &mut [u8]) { + bessie::encrypt_to_slice(key, plaintext, ciphertext) +} + +/// Encrypt a message and return the ciphertext as a `Vec`. +pub fn encrypt(key: &Key, plaintext: &[u8]) -> Vec { + bessie::encrypt(key, plaintext) +} + +/// Decrypt a message and return the plaintext as `Result` of `Vec`. +/// +/// If the ciphertext or key has been changed, decryption will return `Err`. +pub fn decrypt(key: &Key, ciphertext: &[u8]) -> Result> { + bessie::decrypt(key, ciphertext).map_err(|err| Error::Generic(err.to_string())) +} diff --git a/kytes/src/crypto/keys.rs b/kytes/src/crypto/keys.rs new file mode 100644 index 0000000..0bb4d76 --- /dev/null +++ b/kytes/src/crypto/keys.rs @@ -0,0 +1,8 @@ +//! Keypairs. + +use crate::crypto::Key; + +/// Generate a random secret seed. +pub fn generate_seed() -> Key { + rand::random() +} diff --git a/kytes/src/crypto/mod.rs b/kytes/src/crypto/mod.rs new file mode 100644 index 0000000..85cb1bd --- /dev/null +++ b/kytes/src/crypto/mod.rs @@ -0,0 +1,9 @@ +mod encryption; +mod keys; +mod passphrase; +mod seed; + +/// A 32 bytes key (encryption key or public key or shared_secret key). +pub type Key = [u8; bessie::KEY_LEN]; +/// A 24 bytes Nonce or salt. +pub type Nonce = [u8; bessie::NONCE_LEN]; diff --git a/kytes/src/passphrase.rs b/kytes/src/crypto/passphrase.rs similarity index 99% rename from kytes/src/passphrase.rs rename to kytes/src/crypto/passphrase.rs index 39d3060..3872e5b 100644 --- a/kytes/src/passphrase.rs +++ b/kytes/src/crypto/passphrase.rs @@ -1,3 +1,5 @@ +//! EFF large wordlist passphrase generator. + use rand::seq::SliceRandom; /// Generate a 4 words passphrase with spaces, from the [EFF large wordlist](https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt). diff --git a/kytes/src/crypto/seed.rs b/kytes/src/crypto/seed.rs new file mode 100644 index 0000000..49ca59b --- /dev/null +++ b/kytes/src/crypto/seed.rs @@ -0,0 +1,124 @@ +//! Manage Kytes seed files. +//! +//! Seed file contains a seed encrypted with a strong passphrase. + +use argon2::PasswordHasher; +use bytes::{BufMut, Bytes, BytesMut}; + +use crate::{ + crypto::{ + encryption::{ciphertext_len, decrypt, encrypt_to_slice}, + keys::generate_seed, + passphrase::generate_4words_passphrase, + Key, Nonce, + }, + Error, Result, +}; + +const SEED_SCHEME: &[u8] = b"kytes:seed:"; + +const VERSION: u8 = 0; +const KNOWN_VERSIONS: [u8; 1] = [0]; + +/// Encrypt the seed with a strong passphrase, and return an [encrypted seed +/// file](../../../design/seed.md). +pub fn encrypt_seed(seed: &Key, passphrase: &str) -> Bytes { + let encryption_key = derive_encrypiton_key(passphrase); + + let mut seed_file = BytesMut::with_capacity(SEED_SCHEME.len() + 33); + seed_file.extend_from_slice(SEED_SCHEME); + + let suffix_len = 1 + ciphertext_len(seed.len() as u64).unwrap() as usize; + let mut suffix = vec![0_u8; suffix_len]; + + suffix[0] = VERSION; + encrypt_to_slice(&encryption_key, seed, &mut suffix[1..]); + + seed_file.extend_from_slice(z32::encode(&suffix).as_bytes()); + + seed_file.freeze() +} + +pub fn decrypt_seed(seed_file: Bytes, passphrase: &str) -> Result> { + if !seed_file.starts_with(SEED_SCHEME) { + return Err(Error::Generic("Not a Kytes seed".to_string())); + } + + let suffix = z32::decode(&seed_file[SEED_SCHEME.len()..]) + .map_err(|_| Error::Generic("Invalid seed encoding".to_string()))?; + + let version = suffix[0]; + + match version { + 0 => decrypted_seed_v0(&suffix, passphrase), + _ => Err(Error::Generic( + "Unknown kytes seed file version".to_string(), + )), + } +} + +fn decrypted_seed_v0(suffix: &[u8], passphrase: &str) -> Result> { + let encryption_key = derive_encrypiton_key(passphrase); + let encrypted_seed = &suffix[1..]; + + decrypt(&encryption_key, encrypted_seed) +} + +fn parse_version(byte_string: &[u8]) -> Result { + // Convert byte array to string slice + let str_slice = std::str::from_utf8(byte_string) + .map_err(|_| Error::Generic("Invalid version number".to_string()))?; + + str_slice + .parse::() + .map(Ok) + .map_err(|_| Error::Generic("Invalid version number".to_string()))? +} + +/// Derive a secret key from a strong passphrase for encrypting/decrypting the seed. +fn derive_encrypiton_key(passphrase: &str) -> Key { + // Argon2 with default params (Argon2id v19) + let hasher = argon2::Argon2::default(); + + let mut encryption_key: Key = [0; 32]; + + hasher + .hash_password_into( + passphrase.as_bytes(), + // While this is technically a Nonce reuse, it should not be a problem + // since the encryption key is never shared or stored anywhere. + SEED_SCHEME, + &mut encryption_key, + ) + // There shouldn't be any error, as we use the default params. + .unwrap(); + + encryption_key +} + +#[cfg(test)] +mod test { + use std::time::Instant; + + use super::*; + + #[test] + fn test_encrypt_decrypt_seed() { + let seed = generate_seed(); + let passphrase = generate_4words_passphrase(); + + let encrypted_seed_file = encrypt_seed(&seed, &passphrase); + + dbg!(&encrypted_seed_file); + + let start = Instant::now(); + let decrypted_seed = decrypt_seed(encrypted_seed_file, &passphrase) + .expect("Failde to decrypt the seed file"); + + assert!( + start.elapsed().as_millis() > 300, + "decrypting the seed shouldn't be too fast" + ); + assert_eq!(decrypted_seed, seed); + } +} diff --git a/kytes/src/error.rs b/kytes/src/error.rs new file mode 100644 index 0000000..28339e4 --- /dev/null +++ b/kytes/src/error.rs @@ -0,0 +1,9 @@ +//! Main Crate Error + +#[derive(thiserror::Error, Debug)] +/// Kytes crate error enum. +pub enum Error { + /// For starter, to remove as code matures. + #[error("Generic error: {0}")] + Generic(String), +} diff --git a/kytes/src/lib.rs b/kytes/src/lib.rs index 1831a13..e76471b 100644 --- a/kytes/src/lib.rs +++ b/kytes/src/lib.rs @@ -1,3 +1,9 @@ -#[allow(unused)] +#![allow(unused)] mod crypto; -mod passphrase; +mod error; + +// Exports +pub use crate::error::Error; + +// Alias Result to be the crate Result. +pub type Result = core::result::Result; diff --git a/kytes/src/main.rs b/kytes/src/main.rs index fccbbed..12e3a91 100644 --- a/kytes/src/main.rs +++ b/kytes/src/main.rs @@ -1,8 +1,66 @@ -mod passphrase; +use clap::{Parser, Subcommand}; -use passphrase::generate_4words_passphrase; +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Optional name to operate on + name: Option, + + /// Sets a custom config file + // #[arg(short, long, value_name = "FILE")] + // config: Option, + + /// Turn debugging information on + #[arg(short, long, action = clap::ArgAction::Count)] + debug: u8, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// does testing things + Test { + /// lists test values + #[arg(short, long)] + list: bool, + }, +} fn main() { - println!("Hello, world!"); - println!("{}", generate_4words_passphrase()); + let cli = Cli::parse(); + + // You can check the value provided by positional arguments, or option arguments + if let Some(name) = cli.name.as_deref() { + println!("Value for name: {name}"); + } + + // if let Some(config_path) = cli.config.as_deref() { + // println!("Value for config: {}", config_path.display()); + // } + + // You can see how many times a particular flag or argument occurred + // Note, only flags can have multiple occurrences + match cli.debug { + 0 => println!("Debug mode is off"), + 1 => println!("Debug mode is kind of on"), + 2 => println!("Debug mode is on"), + _ => println!("Don't be crazy"), + } + + // You can check for the existence of subcommands, and if found use their + // matches just as you would the top level cmd + match &cli.command { + Some(Commands::Test { list }) => { + if *list { + println!("Printing testing lists..."); + } else { + println!("Not printing testing lists..."); + } + } + None => {} + } + + // Continued program logic goes here... }