use crate::errors::{CoreError, CoreResult};
use sui_id_store::crypto::{open, seal, MasterKey};
use sui_id_store::repos::{
email_outbox, refresh_tokens, signing_keys, smtp_config, user_totp, user_webauthn_credentials,
};
use sui_id_store::Database;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct RotationReport {
pub signing_keys: u64,
pub refresh_tokens: u64,
pub user_totp_secrets: u64,
pub user_totp_recovery_codes: u64,
pub user_webauthn_credentials: u64,
pub smtp_config: u64,
pub email_outbox_rows: u64,
}
impl RotationReport {
pub fn total(&self) -> u64 {
self.signing_keys
+ self.refresh_tokens
+ self.user_totp_secrets
+ self.user_totp_recovery_codes
+ self.user_webauthn_credentials
+ self.smtp_config
+ self.email_outbox_rows
}
}
pub async fn rotate_master_key(
db: &Database,
new_key: &MasterKey,
) -> CoreResult<RotationReport> {
let old_key = db.key();
let mut report = RotationReport::default();
db.with_tx_sync(|tx| {
report.signing_keys = signing_keys::reseal_all(tx, old_key, new_key)?;
report.refresh_tokens = refresh_tokens::reseal_all(tx, old_key, new_key)?;
let (totp_n, recovery_n) = user_totp::reseal_all(tx, old_key, new_key)?;
report.user_totp_secrets = totp_n;
report.user_totp_recovery_codes = recovery_n;
report.user_webauthn_credentials =
user_webauthn_credentials::reseal_all(tx, old_key, new_key)?;
report.smtp_config = smtp_config::reseal_all(tx, old_key, new_key)?;
report.email_outbox_rows = email_outbox::reseal_all(tx, old_key, new_key)? as u64;
Ok(())
})?;
Ok(report)
}
pub async fn reseal_one(
old_key: &MasterKey,
new_key: &MasterKey,
sealed: &[u8],
aad: &[u8],
) -> CoreResult<Vec<u8>> {
let plaintext = open(old_key, sealed, aad)
.map_err(|_| CoreError::BadRequest("decrypt with old key failed".into()))?;
let resealed = seal(new_key, &plaintext, aad)
.map_err(|_| CoreError::Internal)?;
Ok(resealed)
}
#[cfg(test)]
mod tests {
use super::*;
use sui_id_store::crypto::MasterKey;
#[tokio::test]
async fn reseal_one_round_trip() {
let old = MasterKey::generate();
let new = MasterKey::generate();
let aad = b"test-aad";
let plaintext = b"hello world".to_vec();
let sealed = seal(&old, &plaintext, aad).expect("seal");
let resealed = reseal_one(&old, &new, &sealed, aad).await.expect("reseal");
let opened_new = open(&new, &resealed, aad).expect("open with new");
assert_eq!(opened_new, plaintext);
assert!(open(&old, &resealed, aad).is_err());
}
#[tokio::test]
async fn reseal_one_fails_with_wrong_old_key() {
let real_old = MasterKey::generate();
let wrong_old = MasterKey::generate();
let new = MasterKey::generate();
let aad = b"test-aad";
let sealed = seal(&real_old, b"data", aad).expect("seal");
assert!(reseal_one(&wrong_old, &new, &sealed, aad).await.is_err());
}
#[tokio::test]
async fn reseal_one_with_wrong_aad_fails() {
let old = MasterKey::generate();
let new = MasterKey::generate();
let sealed = seal(&old, b"data", b"correct-aad").expect("seal");
assert!(reseal_one(&old, &new, &sealed, b"wrong-aad").await.is_err());
}
#[tokio::test]
async fn rotation_report_total_sums_columns() {
let r = RotationReport {
signing_keys: 1,
refresh_tokens: 5,
user_totp_secrets: 3,
user_totp_recovery_codes: 3,
user_webauthn_credentials: 2,
smtp_config: 1,
email_outbox_rows: 0,
};
assert_eq!(r.total(), 15);
}
}