use cratefield_core::Statement;
use sea_query::Value as SeaValue;
use crate::{Access, Actor, SecretStore, SecretsError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RotationReport {
pub from_key: Option<String>,
pub to_key: Option<String>,
pub reencrypted: usize,
pub retired_old: bool,
pub planned: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RewrapReport {
pub keys: usize,
pub key_ref: String,
pub planned: bool,
}
impl SecretStore {
pub async fn rotate_dek(
&self,
actor: &Actor,
plan: bool,
) -> Result<RotationReport, SecretsError> {
let result = self.rotate_dek_inner(plan).await;
self.audit_action(Access::RotateDek, actor, result.is_ok())
.await?;
result
}
async fn rotate_dek_inner(&self, plan: bool) -> Result<RotationReport, SecretsError> {
let (old_key_id, old_dek) = self.active_key().await?;
let live = self.live_versions().await?;
if plan {
return Ok(RotationReport {
from_key: Some(old_key_id),
to_key: None,
reencrypted: live.len(),
retired_old: false,
planned: true,
});
}
let (new_key_id, new_dek, insert) = self.prepare_key("active").await?;
self.db()
.batch(&[
Statement::with_values(
"UPDATE harness_secret_keys SET state = 'retiring' WHERE key_id = ?",
vec![text(&old_key_id)],
),
insert,
])
.await?;
let mut reencrypted = 0;
for (name, version) in &live {
let plaintext = self
.open_version(name, *version, &old_key_id, &old_dek)
.await?;
let (nonce, ciphertext) =
self.seal(name, *version, &new_key_id, &new_dek, plaintext.expose())?;
self.db()
.execute(&Statement::with_values(
"UPDATE harness_secrets SET key_id = ?, nonce = ?, ciphertext = ? \
WHERE name = ? AND version = ?",
vec![
text(&new_key_id),
blob(nonce),
blob(ciphertext),
text(name),
SeaValue::BigInt(Some(i64::from(*version))),
],
))
.await?;
reencrypted += 1;
}
let remaining = self.versions_under(&old_key_id).await?;
let retired_old = remaining == 0;
if retired_old {
self.db()
.execute(&Statement::with_values(
"UPDATE harness_secret_keys SET state = 'retired' WHERE key_id = ?",
vec![text(&old_key_id)],
))
.await?;
}
Ok(RotationReport {
from_key: Some(old_key_id),
to_key: Some(new_key_id),
reencrypted,
retired_old,
planned: false,
})
}
pub async fn rewrap(&self, actor: &Actor, plan: bool) -> Result<RewrapReport, SecretsError> {
let result = self.rewrap_inner(plan).await;
self.audit_action(Access::Rewrap, actor, result.is_ok())
.await?;
result
}
async fn rewrap_inner(&self, plan: bool) -> Result<RewrapReport, SecretsError> {
let rows = self
.db()
.query(&Statement::new(
"SELECT key_id FROM harness_secret_keys WHERE state <> 'retired' ORDER BY key_id",
))
.await?;
let key_ids: Vec<String> = rows
.rows
.iter()
.filter_map(|row| row.get::<String>("key_id"))
.collect();
if plan {
return Ok(RewrapReport {
keys: key_ids.len(),
key_ref: self.kms().key_ref().to_owned(),
planned: true,
});
}
for key_id in &key_ids {
let dek = self.key(key_id).await?;
let wrapped = self.kms().wrap(&dek).await?;
self.db()
.execute(&Statement::with_values(
"UPDATE harness_secret_keys SET wrapped_dek = ?, kms_provider = ?, \
kms_key_ref = ? WHERE key_id = ?",
vec![
blob(wrapped),
text(self.kms().provider()),
text(self.kms().key_ref()),
text(key_id),
],
))
.await?;
}
Ok(RewrapReport {
keys: key_ids.len(),
key_ref: self.kms().key_ref().to_owned(),
planned: false,
})
}
async fn live_versions(&self) -> Result<Vec<(String, u32)>, SecretsError> {
let rows = self
.db()
.query(&Statement::new(
"SELECT name, version FROM harness_secrets WHERE deleted_at IS NULL \
ORDER BY name ASC, version ASC",
))
.await?;
let mut out = Vec::with_capacity(rows.rows.len());
for row in &rows.rows {
let name: String = row
.get("name")
.ok_or_else(|| SecretsError::Invalid("a secret row has no name".to_owned()))?;
let version: i64 = row
.get("version")
.ok_or_else(|| SecretsError::Invalid("a secret row has no version".to_owned()))?;
let version = u32::try_from(version)
.map_err(|_| SecretsError::Invalid("a version is out of range".to_owned()))?;
out.push((name, version));
}
Ok(out)
}
async fn versions_under(&self, key_id: &str) -> Result<i64, SecretsError> {
let rows = self
.db()
.query(&Statement::with_values(
"SELECT COUNT(*) AS n FROM harness_secrets WHERE key_id = ?",
vec![text(key_id)],
))
.await?;
Ok(rows
.first()
.and_then(|row| row.get::<i64>("n"))
.unwrap_or(0))
}
}
fn text(value: &str) -> SeaValue {
SeaValue::String(Some(Box::new(value.to_owned())))
}
fn blob(value: Vec<u8>) -> SeaValue {
SeaValue::Bytes(Some(Box::new(value)))
}