563bb99
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<String> {
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<String> {
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<models::space::spaces::Model> {
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::Model> {
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::Model> {
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::Model> {
models::ci::workflow_artifact::Entity::find_by_id(artifact_id)
.one(self.db_read())
.await?
.ok_or(CiError::NotFound)
}
}