use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng}; use aes_gcm::{Aes256Gcm, Nonce}; use base64::Engine; use base64::engine::general_purpose::STANDARD; use sha2::{Digest, Sha256}; use crate::errors::{CiError, CiResult}; fn key_material(secret_key: &str) -> [u8; 32] { Sha256::digest(secret_key.as_bytes()).into() } pub fn encrypt(secret_key: &str, plaintext: &str) -> CiResult { let key = key_material(secret_key); let cipher = Aes256Gcm::new((&key).into()); let nonce = Aes256Gcm::generate_nonce(&mut OsRng); let ciphertext = cipher .encrypt(&nonce, plaintext.as_bytes()) .map_err(|_| CiError::Internal("secret encryption failed".to_string()))?; let mut output = Vec::with_capacity(nonce.len() + ciphertext.len()); output.extend_from_slice(&nonce); output.extend_from_slice(&ciphertext); Ok(STANDARD.encode(output)) } pub fn decrypt(secret_key: &str, stored: &str) -> CiResult { let bytes = STANDARD .decode(stored) .map_err(|_| CiError::Internal("malformed encrypted secret".to_string()))?; if bytes.len() < 12 { return Err(CiError::Internal("malformed encrypted secret".to_string())); } let (nonce_bytes, ciphertext) = bytes.split_at(12); let key = key_material(secret_key); let cipher = Aes256Gcm::new((&key).into()); let mut nonce_arr = [0u8; 12]; nonce_arr.copy_from_slice(nonce_bytes); let nonce = Nonce::from(nonce_arr); let plaintext = cipher .decrypt(&nonce, ciphertext) .map_err(|_| CiError::Internal("secret decryption failed".to_string()))?; String::from_utf8(plaintext) .map_err(|_| CiError::Internal("decrypted secret is not utf8".to_string())) } pub fn sha256_hex(data: &[u8]) -> String { let digest = Sha256::digest(data); let mut out = String::with_capacity(digest.len() * 2); for byte in digest { out.push_str(&format!("{byte:02x}")); } out } pub fn random_secret(bytes: usize) -> String { use rand::RngCore; let mut buf = vec![0u8; bytes]; rand::thread_rng().fill_bytes(&mut buf); STANDARD.encode(buf) } pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; } let mut diff = 0u8; for (x, y) in a.iter().zip(b.iter()) { diff |= x ^ y; } diff == 0 } #[must_use] pub fn redact(text: &str, secrets: &[String]) -> String { let mut out = text.to_string(); for secret in secrets { if secret.len() < 4 { continue; } if out.contains(secret.as_str()) { out = out.replace(secret.as_str(), "***"); } } out } use sea_orm::ColumnTrait; use sea_orm::EntityTrait; use sea_orm::QueryFilter; impl crate::service::CiService { pub async fn delete_repo_secret(&self, repo_id: uuid::Uuid, name: &str) -> CiResult<()> { let row = models::ci::ci_secret::Entity::find() .filter(models::ci::ci_secret::Column::RepoId.eq(repo_id)) .filter(models::ci::ci_secret::Column::Name.eq(name.to_string())) .one(self.db_read()) .await?; let Some(row) = row else { return Err(CiError::NotFound); }; self.delete_secret(row.id).await } pub async fn resolve_space(&self, space_name: &str) -> CiResult { use models::space::spaces; let lower = space_name.trim().to_lowercase(); if lower.is_empty() { return Err(CiError::NotFound); } spaces::Entity::find() .filter(spaces::Column::LowerName.eq(lower)) .filter(spaces::Column::IsActive.eq(true)) .one(self.db_read()) .await? .ok_or(CiError::NotFound) } pub async fn find_environment( &self, space_id: uuid::Uuid, name: &str, ) -> CiResult { models::ci::ci_environment::Entity::find() .filter(models::ci::ci_environment::Column::SpaceId.eq(space_id)) .filter(models::ci::ci_environment::Column::Name.eq(name.to_string())) .one(self.db_read()) .await? .ok_or(CiError::NotFound) } pub async fn job_row(&self, job_id: uuid::Uuid) -> CiResult { models::ci::workflow_job::Entity::find_by_id(job_id) .one(self.db_read()) .await? .ok_or(CiError::NotFound) } pub async fn artifact_row( &self, artifact_id: uuid::Uuid, ) -> CiResult { models::ci::workflow_artifact::Entity::find_by_id(artifact_id) .one(self.db_read()) .await? .ok_or(CiError::NotFound) } }