From e70a68db98099dc4c26a476d2537f84495831f10 Mon Sep 17 00:00:00 2001 From: oluceps Date: Mon, 16 Sep 2024 01:46:29 +0800 Subject: [PATCH] + + + - --- Cargo.lock | 32 ++++ Cargo.toml | 1 + TODO.md | 4 +- src/cmd/deploy.rs | 36 ++-- src/cmd/renc.rs | 176 ++++++------------ src/cmd/stored_sec_path.rs | 40 +++- ...06ab468373d36c092aebf73-factorio-admin.age | 13 +- ...974360dd2d93e3de475a84-factorio-server.age | Bin 302 -> 376 bytes 8 files changed, 155 insertions(+), 147 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 166497a..eee3a28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,18 @@ dependencies = [ "serde", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "atomic" version = "0.5.3" @@ -197,6 +209,19 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -316,6 +341,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "cookie-factory" version = "0.3.3" @@ -1631,6 +1662,7 @@ version = "0.1.0" dependencies = [ "age", "argh", + "blake3", "eyre", "serde", "sha2", diff --git a/Cargo.toml b/Cargo.toml index bcd97a1..e893289 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] age = {version = "0.10.0",features = ["ssh"]} argh = "0.1.12" +blake3 = "1.5.4" eyre = "0.6.12" serde = "1.0.210" sha2 = "0.10.8" diff --git a/TODO.md b/TODO.md index 6ce1b13..1d23f96 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,8 @@ - [x] should get entire Secret when deploy -- [ ] remove renced path while renc +- [ ] skip renc +- [x] remove renced path while renc - [ ] permission set +- [ ] age plugin - [ ] check command - [ ] nix integration - [ ] feed the toml after renced, thus store path changed diff --git a/src/cmd/deploy.rs b/src/cmd/deploy.rs index 090b177..9af27d5 100644 --- a/src/cmd/deploy.rs +++ b/src/cmd/deploy.rs @@ -9,13 +9,24 @@ use std::{ use crate::{ cmd::stored_sec_path::SecretPathMap, - profile::{self, Profile}, + profile::{self, HostKey, Profile}, }; use age::x25519; use eyre::{eyre, Context, Result}; use spdlog::{debug, error, info, trace}; +impl HostKey { + pub fn get_identity(&self) -> Result { + fs::read_to_string(&self.path) + .wrap_err_with(|| eyre!("reading ssh host key error: {}", self.path)) + .and_then(|i| { + age::ssh::Identity::from_buffer(i.as_bytes(), Some(String::from("thekey"))) + .map_err(|e| eyre!("convert age identity from ssh key error: {}", e)) + }) + } +} + const KEY_TYPE: &str = "ed25519"; impl Profile { pub fn get_decrypted_mount_point_path(&self) -> String { @@ -35,12 +46,7 @@ impl Profile { .iter() .find(|i| i.r#type == KEY_TYPE) { - fs::read_to_string(&k.path) - .wrap_err_with(|| eyre!("reading ssh host key error: {}", k.path)) - .and_then(|i| { - age::ssh::Identity::from_buffer(i.as_bytes(), Some(String::from("thekey"))) - .map_err(|e| eyre!("convert age identity from ssh key error: {}", e)) - }) + k.get_identity() } else { Err(eyre!("key with type {} not found", KEY_TYPE)) } @@ -74,7 +80,7 @@ impl Profile { error!("parse mount point generation err: {:?}", e) } Ok(res) => { - info!("found mountpoint generation {}", res); + debug!("found mountpoint generation {}", res); if res >= max { max = res + 1; } @@ -93,10 +99,14 @@ impl Profile { pub fn deploy(self) -> Result<()> { // secrets => vec let sec_ciphertext_map: HashMap> = { - let map = SecretPathMap::init_from(&self).inner(); + let map = SecretPathMap::init_from_to_renced_store_path(&self).inner(); let mut ret = HashMap::new(); map.into_iter().for_each(|(s, p)| { - let _ = ret.insert(s, p.read_to_cipher_content().expect("read error")); + let _ = ret.insert( + s, + p.read_hostpubkey_encrypted_cipher_content() + .expect("read error"), + ); }); ret }; @@ -132,6 +142,10 @@ impl Profile { let _ = reader.read_to_end(&mut decrypted); + info!( + "start deploying {} to generation {}", + n.name, generation_count + ); let mut the_file_fd = { let mut p = target_extract_dir_with_gen.clone(); p.push(n.name); @@ -150,7 +164,7 @@ impl Profile { .wrap_err("create symlink error") .is_ok() { - info!("deploy secrets success"); + info!("deployment success"); } Ok(()) } diff --git a/src/cmd/renc.rs b/src/cmd/renc.rs index 4d6a47e..0063a1f 100644 --- a/src/cmd/renc.rs +++ b/src/cmd/renc.rs @@ -1,7 +1,6 @@ use eyre::{eyre, ContextCompat, Result}; use spdlog::{debug, error, info}; use std::{ - collections::{HashMap, HashSet}, fs::{self, File}, io::{Read, Write}, iter, @@ -9,7 +8,10 @@ use std::{ str::FromStr, }; -use crate::profile::{MasterIdentity, Profile, Settings}; +use crate::{ + cmd::stored_sec_path::{SecretBufferMap, SecretPathMap}, + profile::{MasterIdentity, Profile, Settings}, +}; use crate::{interop::add_to_store, profile}; impl profile::Secret { @@ -24,82 +26,10 @@ pub struct NamePathPair(String, PathBuf); #[derive(Hash, Debug, Eq, PartialEq)] pub struct NamePathPairList(Vec); -impl NamePathPairList { - pub fn inner(self) -> Vec { - self.0 - } - /// Vec => Map - pub fn into_map(self) -> HashMap { - let mut renc_path_map = HashMap::new(); - for i in self.inner() { - let _ = renc_path_map.insert(i.name(), i.path()); - } - renc_path_map - } -} - -impl NamePathPair { - fn name(&self) -> String { - self.0.clone() - } - fn path(self) -> PathBuf { - self.1 - } -} - -#[derive(Hash, Debug, Eq, PartialEq, Clone)] -pub struct NameBufPair(String, Vec); - -impl NameBufPair { - fn name(&self) -> String { - self.0.clone() - } - fn buf(self) -> Vec { - self.1 - } - fn from(raw: (String, Vec)) -> Self { - Self(raw.0, raw.1) - } -} - use age::x25519; use super::stored_sec_path::StoredSecretPath; impl Profile { - /// Get the `secrets.{}.file`, which in nix store - pub fn get_cipher_file_paths(&self) -> HashSet { - let mut sec_set = HashSet::new(); - for (name, i) in &self.secrets { - if sec_set.insert(NamePathPair(name.to_owned(), PathBuf::from(i.file.clone()))) { - debug!("found cipher file path {}", i.file) - } - } - sec_set - } - - /// Read - pub fn get_cipher_contents(&self) -> HashSet { - self.get_cipher_file_paths() - .iter() - .map(|i| NameBufPair(i.0.clone(), fs::read(i.1.clone()).expect("yes"))) - .collect() - } - - pub fn get_renced_store_paths(&self) -> NamePathPairList { - NamePathPairList( - self.secrets - .clone() - .into_values() - .map(|i| { - NamePathPair( - i.to_owned().id, - i.to_renced_store_pathbuf(&self.settings).get(), - ) - }) - .collect(), - ) - } - pub fn get_key_pair_iter<'a>( &'a self, ) -> impl Iterator, Result)> + 'a { @@ -142,7 +72,7 @@ impl Profile { /** First decrypt `./secrets/every` with masterIdentity's privkey, - Then encrypt with host public key separately, output to + Then compare hash with decrypted existing file (using hostKey), encrypt with host public key, output to `./secrets/renced/$host` and add to nix store. */ pub fn renc(self, _all: bool, flake_root: PathBuf) -> Result<()> { @@ -162,80 +92,78 @@ impl Profile { )); }; - let cipher_contents = self.get_cipher_contents(); - let renced_secret_paths: NamePathPairList = self.get_renced_store_paths(); - debug!("secret paths: {:?}", renced_secret_paths); - + let renc_path = { + let mut p = flake_root; + p.push(self.settings.storage_dir_relative.clone()); + info!( + "reading user identity encrypted dir under flake root: {:?}", + p + ); + p + }; let mut key_pair_list = self.get_key_pair_iter(); + let sec_buf: SecretBufferMap = + SecretPathMap::init_from_to_user_ident_encrypted_instore_file(&self).into(); if let Some(o) = key_pair_list.find(|k| k.0.is_some()) { let key = o.0.clone().expect("some"); - let decrypted = { - let raw = cipher_contents - .iter() - .map(|i| { - let decryptor = - match age::Decryptor::new(&i.1[..]).expect("parse cipher text error") { - age::Decryptor::Recipients(d) => d, - _ => unreachable!(), - }; - - let mut decrypted = vec![]; - let mut reader = decryptor - .decrypt(iter::once(&key as &dyn age::Identity)) - .unwrap(); - - let _ = reader.read_to_end(&mut decrypted); - (i.name(), decrypted) - }) - .collect::)>>(); - raw.into_iter().map(|i| NameBufPair::from(i)) - }; + let sec_buf = sec_buf.inner(); + let decrypted_iter = sec_buf.iter().map(|(s, b)| { + let decryptor = match age::Decryptor::new(&b[..]).expect("parse cipher text error") + { + age::Decryptor::Recipients(d) => d, + _ => unreachable!(), + }; + + let mut decrypted = vec![]; + let mut reader = decryptor + .decrypt(iter::once(&key as &dyn age::Identity)) + .unwrap(); + + let _ = reader.read_to_end(&mut decrypted); + (s, decrypted) + }); let recip_host_pubkey = ssh::Recipient::from_str(self.settings.host_pubkey.as_str()); let recip_unwrap = recip_host_pubkey.unwrap(); - let encrypted = decrypted.map(|i| { + let encrypted_iter = decrypted_iter.map(|(s, b)| { let encryptor = age::Encryptor::with_recipients(vec![Box::new(recip_unwrap.clone())]) .expect("a recipient"); - let NameBufPair(name, buf) = i; let mut out_buf = vec![]; let mut writer = encryptor.wrap_output(&mut out_buf).unwrap(); - writer.write_all(&buf[..]).unwrap(); + writer.write_all(&b[..]).unwrap(); writer.finish().unwrap(); - NameBufPair(name, out_buf) + (s, out_buf) }); - debug!("re encrypted: {:?}", encrypted); - let renc_path_map = renced_secret_paths.into_map(); + debug!("re encrypted: {:?}", encrypted_iter); - let renc_path = { - let mut p = flake_root; - p.push(self.settings.storage_dir_relative.clone()); - info!("reading dir {:?}", p); - p - }; - if !renc_path.exists() { - let _ = fs::create_dir_all(&renc_path); - } - for i in encrypted { - let base_path = renc_path_map.get(i.name().as_str()); + info!("cleaning old re-encryption extract dir"); + let _ = fs::remove_dir_all(&renc_path); + fs::create_dir_all(&renc_path)?; + let ren = SecretPathMap::init_from_to_renced_store_path(&self).inner(); + encrypted_iter.for_each(|(s, b)| { + // let base_path = sec_path.clone().inner().get(s).cloned(); let mut to_create = renc_path.clone(); - if let Some(n) = base_path { - to_create.push(n.file_name().unwrap()); + // if let Some(n) = base_path { + // get store path from to_renced + let renced_store_path = ren.get(s).cloned().unwrap().inner(); + to_create.push(renced_store_path.file_name().unwrap()); + + debug!("path string {:?}", to_create); + let mut fd = File::create(to_create).expect("create file error"); + let _ = fd.write_all(&b[..]); + // } + }); - debug!("path string {:?}", to_create); - let mut fd = File::create(to_create)?; - let _ = fd.write_all(&i.buf()[..]); - } - } let o = add_to_store(renc_path)?; if !o.status.success() { error!("Command executed with failing error code"); diff --git a/src/cmd/stored_sec_path.rs b/src/cmd/stored_sec_path.rs index 80c6b4c..b4d9126 100644 --- a/src/cmd/stored_sec_path.rs +++ b/src/cmd/stored_sec_path.rs @@ -6,13 +6,42 @@ use spdlog::{debug, info}; use crate::profile::{self, Profile, Settings}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct StoredSecretPath(PathBuf); +#[derive(Debug, Clone)] pub struct SecretPathMap(HashMap); +pub struct SecretBufferMap(HashMap>); + +impl From for SecretBufferMap { + fn from(m: SecretPathMap) -> Self { + let mut map = HashMap::new(); + m.inner().into_iter().for_each(|(s, p)| { + let v = p.read_hostpubkey_encrypted_cipher_content().unwrap(); + map.insert(s, v); + }); + Self(map) + } +} +impl SecretBufferMap { + pub fn inner(self) -> HashMap> { + self.0 + } +} + impl SecretPathMap { - pub fn init_from(profile: &Profile) -> Self { + pub fn init_from_to_user_ident_encrypted_instore_file(profile: &Profile) -> Self { + let mut m = HashMap::new(); + profile.secrets.iter().for_each(|(_, sec)| { + m.insert( + sec.clone(), + StoredSecretPath(PathBuf::from(sec.file.clone())), + ); + }); + Self(m) + } + pub fn init_from_to_renced_store_path(profile: &Profile) -> Self { let mut m = HashMap::new(); profile.secrets.clone().into_values().for_each(|s| { m.insert(s.clone(), s.to_renced_store_pathbuf(&profile.settings)); @@ -60,17 +89,18 @@ impl StoredSecretPath { let mut storage_dir_path = PathBuf::from(storage_dir_store); debug!("storage dir path prefix: {:?}", storage_dir_path); storage_dir_path.push(format!("{}-{}.age", ident_hash, name)); + debug!("added renced credential: {:?}", storage_dir_path); storage_dir_path }; Self(secret_file_path) } - pub fn read_to_cipher_content(self) -> eyre::Result> { + pub fn read_hostpubkey_encrypted_cipher_content(self) -> eyre::Result> { debug!("reading cipher file: {:?}", self.0); - fs::read(self.0).wrap_err("read cipher file error") + fs::read(self.0).wrap_err(format!("read cipher file error")) } - pub fn get(self) -> PathBuf { + pub fn inner(self) -> PathBuf { self.0 } } diff --git a/test/secrets/renced/tester/61baa5c8806ab468373d36c092aebf73-factorio-admin.age b/test/secrets/renced/tester/61baa5c8806ab468373d36c092aebf73-factorio-admin.age index c9e0e2b..21ddd2b 100644 --- a/test/secrets/renced/tester/61baa5c8806ab468373d36c092aebf73-factorio-admin.age +++ b/test/secrets/renced/tester/61baa5c8806ab468373d36c092aebf73-factorio-admin.age @@ -1,7 +1,8 @@ age-encryption.org/v1 --> ssh-ed25519 IQ9o3A I+j09dWy0qAUsINyGEE8E04kDlXMZLdDpwCCulA8lwA -pyznn3OHcKbLAFW7fHAFQV/0oueljk07LiNNg58VhaU --> -hT(WPC-grease waf 5 i'm6-w7u =7k -d7Q ---- ANh1v+UrSvUsYeIFzsPnJaIr5oSgKwDaLeDGfgGw9O8 -0:|ӲuI]QŠCD_p&s<߷xz \ No newline at end of file +-> ssh-ed25519 IQ9o3A Z9FjZuhx6o6Yx4SjXf38gfSLWyF1uLYF/LZ2Oo0ihX8 +6YBayhK4Z276OrPNbwyOm230g7OlY//kwaZOYk/CVrc +-> snm-grease qVJ69 +JXMZ6ZSiwHdn+n25hsuWY9ijZRkfMZsPdBluwb4spJ6ra7s2QBtodSUjjyNlC/WY +Aijid/EO96VVz8SmySuVuCLpq/m1HuKXgvyfAM6HkCwtaNxGbQ +--- bCxs6iSLg6vaw9DEVN4dZBNc6Y4ZY82dVdD5TMH4u4Y +=\ YVUx6UMru7rb7 \ No newline at end of file diff --git a/test/secrets/renced/tester/745a75ccb0974360dd2d93e3de475a84-factorio-server.age b/test/secrets/renced/tester/745a75ccb0974360dd2d93e3de475a84-factorio-server.age index d86784597ea05bebce4a01ae08955715fdcd1195..6e85379264a923b23c4b79068a9ba557aea79867 100644 GIT binary patch delta 341 zcmZ3-^n+=FPJMY;dcK8YV6u;UkanPDc~F^tnpbF9iH~!TXK^hpru)kkwRUTnS!!rVttgAy9rlDfKQTBV5zfffp=73 zRbgPbzfp=;ag|rTWp;3(L5Oj9Sdn&aiKl0UQ@EMCp=o|nWrU@vpT4`t)PSqH@OIBqhH^o}Bz41w lsWNYmtYUOnXwPtK`!SDhsg?tL4v*IcT@EP)Kq|b1z6w zaCvHHSqf=UP(y5HMM8Q)Yf)`=ST$N^QCE3NZ#Y$TSW7WMMK@DUcT_@ZV^eELO$seO zAZl%EEoX9NVRL05MIbF8Wm_R(RApWYcQ8d^S4&h+GkG>sZGUN5X>4UpLQ`opT4Od= zaZ-6|VsbNCYHW3GG*m@1Zf9{sIZA0k3N0-yAa8eQXEB;nEKpxDn$<{wAtYmc@ RwUi7q7RccBs{|DlEOk*QVVD2_