use anyhow::{Context, Result};
use crate::{
InputKind, RedactionArtifact, RedactionSession, Redactor, RestoreResult,
decrypt_session_from_str, encrypt_session_to_string, ensure_restore_valid,
inspect_encrypted_session, require_external_id, StoredSession,
};
use crate::session::SessionStore;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptedRedactionArtifact {
pub artifact: RedactionArtifact,
pub encrypted_session: String,
pub session_summary: crate::SessionSummary,
}
pub fn redact_text_artifact(
redactor: &Redactor,
text: &str,
input_kind: InputKind,
) -> Result<RedactionArtifact> {
redactor
.redact_artifact_with_input_kind(text, input_kind)
.map_err(anyhow::Error::new)
}
pub fn redact_text_artifact_with_source(
redactor: &Redactor,
text: &str,
input_kind: InputKind,
source_path: &str,
) -> Result<RedactionArtifact> {
redactor
.redact_artifact_with_input_kind_and_source(text, input_kind, Some(source_path))
.map_err(anyhow::Error::new)
}
pub fn redact_text_artifact_with_stateful_session(
redactor: &Redactor,
text: &str,
input_kind: InputKind,
external_id: &str,
store: &dyn SessionStore,
) -> Result<RedactionArtifact> {
let (prior_session, expected_version) = load_stateful_session(store, external_id)?;
let artifact = redactor
.redact_artifact_with_prior_session(text, input_kind, prior_session.as_ref(), Some(external_id))
.map_err(anyhow::Error::new)?;
save_stateful_session(store, external_id, &artifact.session, expected_version.as_deref())?;
Ok(artifact)
}
pub fn redact_text_artifact_with_source_and_stateful_session(
redactor: &Redactor,
text: &str,
input_kind: InputKind,
source_path: &str,
external_id: &str,
store: &dyn SessionStore,
) -> Result<RedactionArtifact> {
let (prior_session, expected_version) = load_stateful_session(store, external_id)?;
let artifact = redactor
.redact_artifact_with_input_kind_source_and_prior_session(
text,
input_kind,
Some(source_path),
prior_session.as_ref(),
Some(external_id),
)
.map_err(anyhow::Error::new)?;
save_stateful_session(store, external_id, &artifact.session, expected_version.as_deref())?;
Ok(artifact)
}
pub fn redact_text_with_encrypted_session(
redactor: &Redactor,
text: &str,
input_kind: InputKind,
passphrase: &str,
) -> Result<EncryptedRedactionArtifact> {
let artifact =
redact_text_artifact(redactor, text, input_kind).context("failed to redact text input")?;
let encrypted_session = encrypt_session_to_string(&artifact.session, passphrase)
.context("failed to encrypt redaction session")?;
let session_summary = inspect_encrypted_session(&encrypted_session)
.context("failed to inspect encrypted session")?;
Ok(EncryptedRedactionArtifact {
artifact,
encrypted_session,
session_summary,
})
}
pub fn redact_text_with_encrypted_session_and_source(
redactor: &Redactor,
text: &str,
input_kind: InputKind,
source_path: &str,
passphrase: &str,
) -> Result<EncryptedRedactionArtifact> {
let artifact = redact_text_artifact_with_source(redactor, text, input_kind, source_path)
.context("failed to redact text input")?;
let encrypted_session = encrypt_session_to_string(&artifact.session, passphrase)
.context("failed to encrypt redaction session")?;
let session_summary = inspect_encrypted_session(&encrypted_session)
.context("failed to inspect encrypted session")?;
Ok(EncryptedRedactionArtifact {
artifact,
encrypted_session,
session_summary,
})
}
pub fn decrypt_redaction_session(
encrypted_session: &str,
passphrase: &str,
) -> Result<RedactionSession> {
decrypt_session_from_str(encrypted_session, passphrase)
.context("failed to decrypt provided session")
}
pub fn restore_text_from_encrypted_session(
redactor: &Redactor,
text: &str,
encrypted_session: &str,
passphrase: &str,
) -> Result<RestoreResult> {
let session = decrypt_redaction_session(encrypted_session, passphrase)?;
let restored = redactor.restore_text(text, &session);
ensure_restore_valid(&restored)?;
Ok(restored)
}
fn load_stateful_session(
store: &dyn SessionStore,
external_id: &str,
) -> Result<(Option<RedactionSession>, Option<String>)> {
let prior = store
.load_latest(external_id)
.with_context(|| format!("failed to load latest session for external_id `{external_id}`"))?;
Ok(split_stored_session(prior))
}
fn save_stateful_session(
store: &dyn SessionStore,
external_id: &str,
session: &RedactionSession,
expected_version: Option<&str>,
) -> Result<()> {
store
.save_latest(external_id, session, expected_version)
.with_context(|| format!("failed to save latest session for external_id `{external_id}`"))?;
Ok(())
}
fn split_stored_session(
stored: Option<StoredSession>,
) -> (Option<RedactionSession>, Option<String>) {
match stored {
Some(stored) => (Some(stored.session), stored.version),
None => (None, None),
}
}
pub fn restore_text_from_store(
redactor: &Redactor,
text: &str,
external_id: &str,
store: &dyn SessionStore,
) -> Result<RestoreResult> {
let session = store
.load_latest(require_external_id(Some(external_id))?)
.with_context(|| format!("failed to load latest session for external_id `{external_id}`"))?
.ok_or_else(|| anyhow::anyhow!("no latest session found for external_id `{external_id}`"))?;
let restored = redactor.restore_text(text, &session.session);
ensure_restore_valid(&restored)?;
Ok(restored)
}
#[cfg(test)]
mod tests {
use super::{
redact_text_artifact, redact_text_with_encrypted_session,
restore_text_from_encrypted_session,
};
use crate::types::FindingKind;
use crate::{InputKind, RedactionPolicy, RedactorBuilder};
fn full_redactor() -> crate::Redactor {
RedactorBuilder::new()
.with_redaction_policy(
RedactionPolicy::default()
.with_kind(FindingKind::Domain, true)
.with_kind(FindingKind::Secret, true)
.with_kind(FindingKind::Url, true),
)
.build()
}
#[test]
fn encrypted_redaction_matches_plain_artifact_output() {
let redactor = full_redactor();
let text = "host=service.example.com secret=EJ2QEVC6AKELW0k2kkVY4NgGKONC";
let plain = redact_text_artifact(&redactor, text, InputKind::Text).expect("plain");
let encrypted =
redact_text_with_encrypted_session(&redactor, text, InputKind::Text, "pass")
.expect("encrypted");
assert_eq!(encrypted.artifact.session.entries.len(), plain.session.entries.len());
assert_eq!(
encrypted
.artifact
.session
.entries
.iter()
.map(|entry| (&entry.kind, &entry.original))
.collect::<Vec<_>>(),
plain
.session
.entries
.iter()
.map(|entry| (&entry.kind, &entry.original))
.collect::<Vec<_>>()
);
assert_ne!(
encrypted.artifact.result.redacted_text,
plain.result.redacted_text
);
}
#[test]
fn encrypted_session_restore_round_trips() {
let redactor = full_redactor();
let text = "host=service.example.com";
let encrypted =
redact_text_with_encrypted_session(&redactor, text, InputKind::Text, "pass")
.expect("encrypted");
let restored = restore_text_from_encrypted_session(
&redactor,
&encrypted.artifact.result.redacted_text,
&encrypted.encrypted_session,
"pass",
)
.expect("restore");
assert_eq!(restored.restored_text, text);
}
}