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}