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