add auth system: users, login, JWT, protected routes
Domain: User entity, AuthPort/PasswordHashPort/SecretStore ports. Adapters: auth (argon2 hashing, JWT tokens), secret-store (env-based), config-sqlite user repository, http-api auth routes + extractors. Application: auth_service. SPA: login page, auth client, protected router.
This commit is contained in:
56
crates/adapters/secret-store/src/lib.rs
Normal file
56
crates/adapters/secret-store/src/lib.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce, aead::Aead};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as B64};
|
||||
use domain::SecretStore;
|
||||
use rand_core::{OsRng, RngCore};
|
||||
|
||||
pub struct AesSecretStore {
|
||||
key: Key<Aes256Gcm>,
|
||||
}
|
||||
|
||||
impl AesSecretStore {
|
||||
pub fn from_env() -> Result<Self, String> {
|
||||
let hex_key = std::env::var("KFRAME_ENCRYPTION_KEY")
|
||||
.map_err(|_| "KFRAME_ENCRYPTION_KEY env var is required".to_string())?;
|
||||
let bytes = hex::decode(&hex_key)
|
||||
.map_err(|e| format!("KFRAME_ENCRYPTION_KEY must be 64 hex chars: {e}"))?;
|
||||
if bytes.len() != 32 {
|
||||
return Err(format!(
|
||||
"KFRAME_ENCRYPTION_KEY must be 32 bytes (64 hex chars), got {}",
|
||||
bytes.len()
|
||||
));
|
||||
}
|
||||
let key = Key::<Aes256Gcm>::from_slice(&bytes);
|
||||
Ok(Self { key: *key })
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretStore for AesSecretStore {
|
||||
fn encrypt(&self, plaintext: &str) -> String {
|
||||
let cipher = Aes256Gcm::new(&self.key);
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, plaintext.as_bytes())
|
||||
.expect("AES-GCM encryption should not fail");
|
||||
let mut combined = nonce_bytes.to_vec();
|
||||
combined.extend(ciphertext);
|
||||
B64.encode(combined)
|
||||
}
|
||||
|
||||
fn decrypt(&self, ciphertext: &str) -> String {
|
||||
let combined = B64
|
||||
.decode(ciphertext)
|
||||
.expect("invalid base64 in encrypted field");
|
||||
if combined.len() < 12 {
|
||||
panic!("encrypted data too short");
|
||||
}
|
||||
let (nonce_bytes, ct) = combined.split_at(12);
|
||||
let cipher = Aes256Gcm::new(&self.key);
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, ct)
|
||||
.expect("AES-GCM decryption failed — wrong key or corrupted data");
|
||||
String::from_utf8(plaintext).expect("decrypted data is not valid UTF-8")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user