Skip to main content

cloudiful_redactor/
service.rs

1use anyhow::{Context, Result};
2
3use crate::session::SessionStore;
4use crate::{
5    InputKind, RedactionArtifact, RedactionSession, Redactor, RestorePermit, RestoreResult,
6    StoredSession, create_restore_permit, decrypt_restore_permit, decrypt_session_from_str,
7    encrypt_restore_permit, encrypt_session_to_string, ensure_restore_valid, require_external_id,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct EncryptedRedactionArtifact {
12    pub artifact: RedactionArtifact,
13    pub encrypted_session: String,
14    pub restore_permit: String,
15    pub session_summary: crate::SessionSummary,
16}
17
18pub fn redact_text_artifact(
19    redactor: &Redactor,
20    text: &str,
21    input_kind: InputKind,
22) -> Result<RedactionArtifact> {
23    redactor
24        .redact_artifact_with_input_kind(text, input_kind)
25        .map_err(anyhow::Error::new)
26}
27
28pub fn redact_text_artifact_with_source(
29    redactor: &Redactor,
30    text: &str,
31    input_kind: InputKind,
32    source_path: &str,
33) -> Result<RedactionArtifact> {
34    redactor
35        .redact_artifact_with_input_kind_and_source(text, input_kind, Some(source_path))
36        .map_err(anyhow::Error::new)
37}
38
39pub async fn redact_text_artifact_with_stateful_session(
40    redactor: &Redactor,
41    text: &str,
42    input_kind: InputKind,
43    external_id: &str,
44    store: &dyn SessionStore,
45) -> Result<RedactionArtifact> {
46    let external_id = require_external_id(Some(external_id))?;
47    let (prior_session, expected_version) = load_stateful_session(store, external_id).await?;
48    let artifact = redactor
49        .redact_artifact_with_prior_session(
50            text,
51            input_kind,
52            prior_session.as_ref(),
53            Some(external_id),
54        )
55        .map_err(anyhow::Error::new)?;
56    save_stateful_session(
57        store,
58        external_id,
59        &artifact.session,
60        expected_version.as_deref(),
61    )
62    .await?;
63    Ok(artifact)
64}
65
66pub async fn redact_text_artifact_with_source_and_stateful_session(
67    redactor: &Redactor,
68    text: &str,
69    input_kind: InputKind,
70    source_path: &str,
71    external_id: &str,
72    store: &dyn SessionStore,
73) -> Result<RedactionArtifact> {
74    let external_id = require_external_id(Some(external_id))?;
75    let (prior_session, expected_version) = load_stateful_session(store, external_id).await?;
76    let artifact = redactor
77        .redact_artifact_with_input_kind_source_and_prior_session(
78            text,
79            input_kind,
80            Some(source_path),
81            prior_session.as_ref(),
82            Some(external_id),
83        )
84        .map_err(anyhow::Error::new)?;
85    save_stateful_session(
86        store,
87        external_id,
88        &artifact.session,
89        expected_version.as_deref(),
90    )
91    .await?;
92    Ok(artifact)
93}
94
95pub fn redact_text_with_encrypted_session(
96    redactor: &Redactor,
97    text: &str,
98    input_kind: InputKind,
99    passphrase: &str,
100) -> Result<EncryptedRedactionArtifact> {
101    let artifact =
102        redact_text_artifact(redactor, text, input_kind).context("failed to redact text input")?;
103    let encrypted_session = encrypt_session_to_string(&artifact.session, passphrase)
104        .context("failed to encrypt redaction session")?;
105    let restore_permit =
106        encrypt_restore_permit(&create_restore_permit(&artifact.session), passphrase)
107            .context("failed to encrypt restore permit")?;
108    let session_summary = crate::SessionSummary::from(&artifact.session);
109
110    Ok(EncryptedRedactionArtifact {
111        artifact,
112        encrypted_session,
113        restore_permit,
114        session_summary,
115    })
116}
117
118pub fn redact_text_with_encrypted_session_and_source(
119    redactor: &Redactor,
120    text: &str,
121    input_kind: InputKind,
122    source_path: &str,
123    passphrase: &str,
124) -> Result<EncryptedRedactionArtifact> {
125    let artifact = redact_text_artifact_with_source(redactor, text, input_kind, source_path)
126        .context("failed to redact text input")?;
127    let encrypted_session = encrypt_session_to_string(&artifact.session, passphrase)
128        .context("failed to encrypt redaction session")?;
129    let restore_permit =
130        encrypt_restore_permit(&create_restore_permit(&artifact.session), passphrase)
131            .context("failed to encrypt restore permit")?;
132    let session_summary = crate::SessionSummary::from(&artifact.session);
133
134    Ok(EncryptedRedactionArtifact {
135        artifact,
136        encrypted_session,
137        restore_permit,
138        session_summary,
139    })
140}
141
142pub fn decrypt_redaction_session(
143    encrypted_session: &str,
144    passphrase: &str,
145) -> Result<RedactionSession> {
146    decrypt_session_from_str(encrypted_session, passphrase)
147        .context("failed to decrypt provided session")
148}
149
150pub fn restore_text_from_encrypted_session(
151    text: &str,
152    encrypted_session: &str,
153    encrypted_permits: &[String],
154    passphrase: &str,
155) -> Result<RestoreResult> {
156    let session = decrypt_redaction_session(encrypted_session, passphrase)?;
157    let permits = decrypt_permits(encrypted_permits, passphrase)?;
158    let restored = crate::RestoreContext::with_permits(&session, &permits)?.restore_text(text);
159    ensure_restore_valid(&restored)?;
160    Ok(restored)
161}
162
163async fn load_stateful_session(
164    store: &dyn SessionStore,
165    external_id: &str,
166) -> Result<(Option<RedactionSession>, Option<String>)> {
167    let prior = store.load_latest(external_id).await.with_context(|| {
168        format!("failed to load latest session for external_id `{external_id}`")
169    })?;
170    Ok(split_stored_session(prior))
171}
172
173async fn save_stateful_session(
174    store: &dyn SessionStore,
175    external_id: &str,
176    session: &RedactionSession,
177    expected_version: Option<&str>,
178) -> Result<()> {
179    store
180        .save_latest(external_id, session, expected_version)
181        .await
182        .with_context(|| {
183            format!("failed to save latest session for external_id `{external_id}`")
184        })?;
185    Ok(())
186}
187
188fn split_stored_session(
189    stored: Option<StoredSession>,
190) -> (Option<RedactionSession>, Option<String>) {
191    match stored {
192        Some(stored) => (Some(stored.session), Some(stored.version)),
193        None => (None, None),
194    }
195}
196
197pub async fn restore_text_from_store(
198    text: &str,
199    external_id: &str,
200    store: &dyn SessionStore,
201    permits: &[RestorePermit],
202) -> Result<RestoreResult> {
203    let session = store
204        .load_latest(require_external_id(Some(external_id))?)
205        .await
206        .with_context(|| format!("failed to load latest session for external_id `{external_id}`"))?
207        .ok_or_else(|| {
208            anyhow::anyhow!("no latest session found for external_id `{external_id}`")
209        })?;
210    let restored =
211        crate::RestoreContext::with_permits(&session.session, permits)?.restore_text(text);
212    ensure_restore_valid(&restored)?;
213    Ok(restored)
214}
215
216pub fn decrypt_permits(encrypted: &[String], passphrase: &str) -> Result<Vec<RestorePermit>> {
217    encrypted
218        .iter()
219        .map(|permit| decrypt_restore_permit(permit, passphrase))
220        .collect()
221}
222
223#[cfg(test)]
224mod tests {
225    use super::{
226        redact_text_artifact, redact_text_with_encrypted_session,
227        restore_text_from_encrypted_session,
228    };
229    use crate::types::FindingKind;
230    use crate::{InputKind, RedactionPolicy, RedactorBuilder};
231
232    fn full_redactor() -> crate::Redactor {
233        RedactorBuilder::new()
234            .with_redaction_policy(
235                RedactionPolicy::default()
236                    .with_kind(FindingKind::Domain, true)
237                    .with_kind(FindingKind::Secret, true)
238                    .with_kind(FindingKind::Url, true),
239            )
240            .build()
241    }
242
243    #[test]
244    fn encrypted_redaction_matches_plain_artifact_output() {
245        let redactor = full_redactor();
246        let text = "host=service.example.com secret=EJ2QEVC6AKELW0k2kkVY4NgGKONC";
247        let plain = redact_text_artifact(&redactor, text, InputKind::Text).expect("plain");
248        let encrypted =
249            redact_text_with_encrypted_session(&redactor, text, InputKind::Text, "pass")
250                .expect("encrypted");
251
252        assert_eq!(
253            encrypted.artifact.session.entries.len(),
254            plain.session.entries.len()
255        );
256        assert_eq!(
257            encrypted
258                .artifact
259                .session
260                .entries
261                .iter()
262                .map(|entry| (&entry.kind, &entry.original))
263                .collect::<Vec<_>>(),
264            plain
265                .session
266                .entries
267                .iter()
268                .map(|entry| (&entry.kind, &entry.original))
269                .collect::<Vec<_>>()
270        );
271        assert_ne!(
272            encrypted.artifact.result.redacted_text,
273            plain.result.redacted_text
274        );
275    }
276
277    #[test]
278    fn encrypted_session_restore_round_trips() {
279        let redactor = full_redactor();
280        let text = "host=service.example.com";
281        let encrypted =
282            redact_text_with_encrypted_session(&redactor, text, InputKind::Text, "pass")
283                .expect("encrypted");
284
285        let restored = restore_text_from_encrypted_session(
286            &encrypted.artifact.result.redacted_text,
287            &encrypted.encrypted_session,
288            &[encrypted.restore_permit],
289            "pass",
290        )
291        .expect("restore");
292
293        assert_eq!(restored.restored_text, text);
294    }
295}