use age::Encryptor; use age::IdentityFile; use miette::Diagnostic; use rand::RngCore; use std::io::Write; use std::iter; use std::os::unix::fs::PermissionsExt; use std::path::Path; use thiserror::Error; #[derive(Error, Debug, Diagnostic)] pub enum Error { #[error("key file not found at path")] #[diagnostic(code(scratchback::key::not_found))] KeyNotFound { path: String, help: Option }, #[error("key file has insecure permissions: {path} ({mode:o})")] #[diagnostic(code(scratchback::key::insecure_permissions))] InsecureKeyPermissions { path: String, mode: u32, help: Option, }, #[error("invalid age secret key format")] #[diagnostic(code(scratchback::key::invalid_format))] #[help("ensure key is in bech32 format (age1...)")] InvalidKeyFormat, #[error("encryption failed")] #[diagnostic(code(scratchback::crypto::encryption_failed))] EncryptionFailed, #[error("database error: {0}")] #[diagnostic(code(scratchback::crypto::database_error))] Database(String), #[error("invalid request: {0}")] #[diagnostic(code(scratchback::crypto::invalid_request))] InvalidRequest(String), } struct SyncRecipient(Box); unsafe impl Sync for SyncRecipient {} pub struct MasterKey { recipient: SyncRecipient, } impl MasterKey { pub fn from_identity(identity: age::x25519::Identity) -> Self { Self { recipient: SyncRecipient(Box::new(identity.to_public())), } } pub fn load_from_file(path: &Path) -> Result { if !path.exists() { return Err(Error::KeyNotFound { path: path.display().to_string(), help: Some("set SCRATCHBACK_AGE_KEY_PATH to a valid key file path".into()), }); } let metadata = std::fs::metadata(path).map_err(|_| Error::KeyNotFound { path: path.display().to_string(), help: None, })?; let mode = metadata.permissions().mode() & 0o777; if mode != 0o600 { return Err(Error::InsecureKeyPermissions { path: path.display().to_string(), mode, help: Some("key file must be mode 0600".into()), }); } let identity_file = IdentityFile::from_file(path.to_string_lossy().to_string()) .map_err(|_| Error::InvalidKeyFormat)?; let mut recipients = identity_file .to_recipients() .map_err(|_| Error::InvalidKeyFormat)?; let recipient = recipients.pop().ok_or(Error::InvalidKeyFormat)?; Ok(Self { recipient: SyncRecipient(recipient), }) } pub fn encrypt_dek(&self, dek: &[u8]) -> Result, Error> { let encryptor = Encryptor::with_recipients(iter::once(&*self.recipient.0 as &dyn age::Recipient)) .map_err(|_| Error::EncryptionFailed)?; let mut encrypted = Vec::new(); let mut writer = encryptor .wrap_output(&mut encrypted) .map_err(|_| Error::EncryptionFailed)?; writer.write_all(dek).map_err(|_| Error::EncryptionFailed)?; writer.finish().map_err(|_| Error::EncryptionFailed)?; Ok(encrypted) } } pub fn load_master_key() -> Result { let key_path = std::env::var("SCRATCHBACK_AGE_KEY_PATH").map_err(|_| Error::KeyNotFound { path: "(not set)".to_string(), help: Some("set SCRATCHBACK_AGE_KEY_PATH environment variable".into()), })?; MasterKey::load_from_file(Path::new(&key_path)) } pub fn generate_dek() -> [u8; 32] { let mut dek = [0u8; 32]; rand::thread_rng().fill_bytes(&mut dek); dek } pub fn encrypt_dek(master_key: &MasterKey, dek: &[u8]) -> Result, Error> { master_key.encrypt_dek(dek) }