use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use rand::{rngs::OsRng, RngCore};
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use std::env;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "secrets")]
pub struct Model {
#[sea_orm(primary_key, column_type = "Text", auto_increment = false)]
pub id: String,
pub name: String,
pub namespace: String,
#[sea_orm(unique, column_type = "Text")]
pub full_name: String,
pub owner: String,
pub owner_ref: Option<String>,
pub encrypted_value: String,
pub nonce: String, pub labels: Option<Json>,
pub created_by: Option<String>,
pub updated_at: DateTimeWithTimeZone,
pub created_at: DateTimeWithTimeZone,
pub expires_at: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
impl Model {
fn get_encryption_key() -> Result<[u8; 32], String> {
let key = env::var("NEBU_ENCRYPTION_KEY")
.map_err(|_| "NEBU_ENCRYPTION_KEY environment variable not set".to_string())?;
if key.len() != 32 {
return Err("NEBU_ENCRYPTION_KEY must be exactly 32 bytes".to_string());
}
let mut result = [0u8; 32];
result.copy_from_slice(key.as_bytes());
Ok(result)
}
pub fn encrypt_value(value: &str) -> Result<(String, String), String> {
let key = Self::get_encryption_key()?;
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| format!("Failed to create cipher: {}", e))?;
let mut nonce_bytes = [0u8; 12]; OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, value.as_bytes())
.map_err(|e| format!("Encryption failed: {}", e))?;
let encrypted_value = BASE64.encode(ciphertext);
let nonce_value = BASE64.encode(nonce_bytes);
Ok((encrypted_value, nonce_value))
}
pub fn decrypt_value(&self) -> Result<String, String> {
let key = Self::get_encryption_key()?;
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| format!("Failed to create cipher: {}", e))?;
let nonce_bytes = BASE64
.decode(self.nonce.as_bytes())
.map_err(|e| format!("Failed to decode nonce: {}", e))?;
let ciphertext = BASE64
.decode(self.encrypted_value.as_bytes())
.map_err(|e| format!("Failed to decode ciphertext: {}", e))?;
if nonce_bytes.len() != 12 {
return Err("Invalid nonce length".to_string());
}
let nonce = Nonce::from_slice(&nonce_bytes);
let plaintext = cipher
.decrypt(nonce, ciphertext.as_ref())
.map_err(|e| format!("Decryption failed: {}", e))?;
String::from_utf8(plaintext)
.map_err(|e| format!("Failed to convert decrypted bytes to string: {}", e))
}
pub fn new(
id: String,
name: String,
namespace: String,
owner: String,
value: &str,
created_by: Option<String>,
labels: Option<Json>,
expires_at: Option<i32>,
) -> Result<Self, String> {
let (encrypted_value, nonce) = Self::encrypt_value(value)?;
let now = chrono::Utc::now().into();
Ok(Self {
id,
name: name.clone(),
namespace: namespace.clone(),
full_name: format!("{namespace}/{name}"),
owner,
owner_ref: None,
encrypted_value,
nonce,
labels,
created_by,
updated_at: now,
created_at: now,
expires_at,
})
}
}