1use crate::legacy::{ElementId, Key, MarkData, ObjectId, OpId, OpType};
2use crate::{ActorId, Automerge, AutomergeError, Change, ChangeHash, ScalarValue};
3use rand::rngs::StdRng;
4use rand::seq::SliceRandom;
5use rand::RngExt;
6use std::collections::{BTreeSet, HashMap};
7
8const SYNTHETIC_TEXT: &[u8] = b"loremipsumdolorsitametconsecteturadipiscingelit";
9
10#[derive(Debug, thiserror::Error)]
12pub enum AnonymizeError {
13 #[error("change {change} appeared before dependency {dependency}")]
15 MissingDependency {
16 change: ChangeHash,
18 dependency: ChangeHash,
20 },
21 #[error(transparent)]
23 Apply(#[from] AutomergeError),
24}
25
26pub fn anonymize(document: &Automerge) -> Result<Automerge, AnonymizeError> {
28 Anonymization::new(rand::make_rng()).anonymize(document)
29}
30
31struct Anonymization {
32 rng: StdRng,
33 structural_permutations: StructuralPermutations,
34 content_permutations: StructuralPermutations,
35 synthetic_text: Vec<u8>,
36 synthetic_position: usize,
37}
38
39impl Anonymization {
40 fn new(mut rng: StdRng) -> Self {
41 let mut synthetic_text = SYNTHETIC_TEXT.to_vec();
42 synthetic_text.shuffle(&mut rng);
43 let synthetic_position = rng.random_range(0..synthetic_text.len());
44 Self {
45 rng,
46 structural_permutations: StructuralPermutations::default(),
47 content_permutations: StructuralPermutations::default(),
48 synthetic_text,
49 synthetic_position,
50 }
51 }
52
53 #[cfg(test)]
54 fn from_seed(seed: [u8; 32]) -> Self {
55 use rand::SeedableRng as _;
56
57 Self::new(StdRng::from_seed(seed))
58 }
59
60 fn anonymize(mut self, document: &Automerge) -> Result<Automerge, AnonymizeError> {
61 let changes = document.get_changes(&[]);
62 let actor_map = self.actor_map(&changes);
63 let mut change_hashes = HashMap::<ChangeHash, ChangeHash>::new();
64 let mut anonymized = Automerge::new_with_encoding(document.text_encoding());
65
66 for change in changes {
67 let old_hash = change.hash();
68 let mut expanded = change.decode();
69 expanded.hash = None;
70 expanded.deps = expanded
71 .deps
72 .iter()
73 .map(|dependency| {
74 change_hashes.get(dependency).copied().ok_or(
75 AnonymizeError::MissingDependency {
76 change: old_hash,
77 dependency: *dependency,
78 },
79 )
80 })
81 .collect::<Result<Vec<_>, _>>()?;
82 expanded.actor_id = mapped_actor(&actor_map, &expanded.actor_id);
83 expanded.time = self.random_i64_other_than(expanded.time);
84 expanded.message = expanded
85 .message
86 .as_deref()
87 .map(|message| self.anonymize_content_string(message));
88 expanded.extra_bytes = self.anonymize_bytes(&expanded.extra_bytes);
89
90 for operation in &mut expanded.operations {
91 self.anonymize_operation(operation, &actor_map);
92 }
93
94 let anonymized_change = Change::from(expanded);
95 let anonymized_hash = anonymized_change.hash();
96 anonymized.apply_changes([anonymized_change])?;
97 change_hashes.insert(old_hash, anonymized_hash);
98 }
99
100 Ok(anonymized)
101 }
102
103 fn actor_map(&mut self, changes: &[Change]) -> HashMap<ActorId, ActorId> {
104 let actors = changes
105 .iter()
106 .flat_map(Change::actors)
107 .cloned()
108 .collect::<BTreeSet<_>>();
109
110 loop {
111 let prefix = self.rng.random::<[u8; 8]>();
112 let mapped = actors
113 .iter()
114 .enumerate()
115 .map(|(rank, actor)| {
116 let mut anonymized = [0_u8; 16];
117 anonymized[..8].copy_from_slice(&prefix);
118 anonymized[8..].copy_from_slice(&(rank as u64).to_be_bytes());
119 (actor.clone(), ActorId::from(anonymized))
120 })
121 .collect::<HashMap<_, _>>();
122 if mapped.values().all(|actor| !actors.contains(actor)) {
123 return mapped;
124 }
125 }
126 }
127
128 fn anonymize_operation(
129 &mut self,
130 operation: &mut crate::legacy::Op,
131 actors: &HashMap<ActorId, ActorId>,
132 ) {
133 map_object_id(&mut operation.obj, actors);
134 match &mut operation.key {
135 Key::Map(key) => *key = self.anonymize_structural_string(key).into(),
136 Key::Seq(ElementId::Id(id)) => map_op_id(id, actors),
137 Key::Seq(ElementId::Head) => {}
138 }
139 operation.pred = operation
140 .pred
141 .iter()
142 .cloned()
143 .map(|mut predecessor| {
144 map_op_id(&mut predecessor, actors);
145 predecessor
146 })
147 .collect();
148
149 match &mut operation.action {
150 OpType::Put(value) => *value = self.anonymize_scalar(value),
151 OpType::Increment(value) => *value = self.random_i64_other_than(*value),
152 OpType::MarkBegin(MarkData { name, value, .. }) => {
153 *name = self.anonymize_structural_string(name).into();
154 *value = self.anonymize_scalar(value);
155 }
156 OpType::Make(_) | OpType::Delete | OpType::MarkEnd(_) => {}
157 }
158 }
159
160 fn anonymize_scalar(&mut self, value: &ScalarValue) -> ScalarValue {
161 match value {
162 ScalarValue::Bytes(bytes) => ScalarValue::Bytes(self.anonymize_bytes(bytes)),
163 ScalarValue::Str(value) => {
164 ScalarValue::from(self.anonymize_content_string(value.as_str()))
165 }
166 ScalarValue::Int(value) => ScalarValue::Int(self.random_i64_other_than(*value)),
167 ScalarValue::Uint(value) => ScalarValue::Uint(self.random_u64_other_than(*value)),
168 ScalarValue::F64(value) => ScalarValue::F64(self.random_f64_other_than(*value)),
169 ScalarValue::Counter(value) => {
170 ScalarValue::counter(self.random_i64_other_than(i64::from(value)))
171 }
172 ScalarValue::Timestamp(value) => {
173 ScalarValue::Timestamp(self.random_i64_other_than(*value))
174 }
175 ScalarValue::Boolean(_) => ScalarValue::Boolean(self.rng.random()),
176 ScalarValue::Unknown { type_code, bytes } => ScalarValue::Unknown {
177 type_code: *type_code,
178 bytes: self.anonymize_bytes(bytes),
179 },
180 ScalarValue::Null => ScalarValue::Null,
181 }
182 }
183
184 fn random_i64_other_than(&mut self, original: i64) -> i64 {
185 loop {
186 let replacement = i64::from(self.rng.random::<i32>());
190 if replacement != original {
191 return replacement;
192 }
193 }
194 }
195
196 fn random_u64_other_than(&mut self, original: u64) -> u64 {
197 loop {
198 let replacement = self.rng.random();
199 if replacement != original {
200 return replacement;
201 }
202 }
203 }
204
205 fn random_f64_other_than(&mut self, original: f64) -> f64 {
206 loop {
207 let replacement = self.rng.random::<f64>();
210 if replacement.to_bits() != original.to_bits() {
211 return replacement;
212 }
213 }
214 }
215
216 fn anonymize_structural_string(&mut self, value: &str) -> String {
217 value
218 .chars()
219 .map(|character| {
220 self.structural_permutations
221 .replace(character, &mut self.rng)
222 })
223 .collect()
224 }
225
226 fn anonymize_content_string(&mut self, value: &str) -> String {
227 let mut result = String::with_capacity(value.len());
228 for character in value.chars() {
229 if character.is_whitespace() || character.is_ascii_control() {
230 result.push(character);
231 } else if character.is_ascii() {
232 result.push(char::from(self.random_synthetic_byte(character as u8)));
233 } else {
234 result.push(self.content_permutations.replace(character, &mut self.rng));
235 }
236 }
237 result
238 }
239
240 fn anonymize_bytes(&mut self, value: &[u8]) -> Vec<u8> {
241 value
242 .iter()
243 .map(|source| self.random_synthetic_byte(*source))
244 .collect()
245 }
246
247 fn random_synthetic_byte(&mut self, original: u8) -> u8 {
248 loop {
249 let replacement = self.synthetic_text[self.synthetic_position];
250 self.synthetic_position = (self.synthetic_position + 1) % self.synthetic_text.len();
251 if replacement != original {
252 return replacement;
253 }
254 }
255 }
256}
257
258#[derive(Default)]
265struct StructuralPermutations {
266 printable_ascii: Option<Vec<u32>>,
267 ascii_control: Option<Vec<u32>>,
268 two_byte: Option<Vec<u32>>,
269 three_byte: Option<Vec<u32>>,
270 four_byte: Option<Vec<u32>>,
271}
272
273impl StructuralPermutations {
274 fn replace(&mut self, character: char, rng: &mut StdRng) -> char {
275 let (alphabet, source_rank, alphabet_size) = structural_character_rank(character);
276 let permutation = match alphabet {
277 StructuralAlphabet::PrintableAscii => &mut self.printable_ascii,
278 StructuralAlphabet::AsciiControl => &mut self.ascii_control,
279 StructuralAlphabet::TwoByte => &mut self.two_byte,
280 StructuralAlphabet::ThreeByte => &mut self.three_byte,
281 StructuralAlphabet::FourByte => &mut self.four_byte,
282 }
283 .get_or_insert_with(|| random_derangement(alphabet_size, rng));
284 let replacement_rank = permutation[source_rank as usize];
285 structural_character_from_rank(character, replacement_rank)
286 }
287}
288
289#[derive(Clone, Copy)]
290enum StructuralAlphabet {
291 PrintableAscii,
292 AsciiControl,
293 TwoByte,
294 ThreeByte,
295 FourByte,
296}
297
298fn random_derangement(count: u32, rng: &mut StdRng) -> Vec<u32> {
299 debug_assert!(count > 1);
300 loop {
301 let mut permutation = (0..count).collect::<Vec<_>>();
302 permutation.shuffle(rng);
303 if permutation
304 .iter()
305 .enumerate()
306 .all(|(index, replacement)| index as u32 != *replacement)
307 {
308 return permutation;
309 }
310 }
311}
312
313fn structural_character_rank(character: char) -> (StructuralAlphabet, u32, u32) {
316 let codepoint = character as u32;
317 match character.len_utf8() {
318 1 if (0x20..=0x7e).contains(&codepoint) => (
319 StructuralAlphabet::PrintableAscii,
320 codepoint - 0x20,
321 0x7e - 0x20 + 1,
322 ),
323 1 if codepoint < 0x20 => (StructuralAlphabet::AsciiControl, codepoint, 0x21),
324 1 => (StructuralAlphabet::AsciiControl, 0x20, 0x21),
325 2 => (StructuralAlphabet::TwoByte, codepoint - 0x80, 0x800 - 0x80),
326 3 if codepoint < 0xd800 => (
327 StructuralAlphabet::ThreeByte,
328 codepoint - 0x800,
329 0xd800 - 0x800 + 0x2000,
330 ),
331 3 => (
332 StructuralAlphabet::ThreeByte,
333 0xd800 - 0x800 + codepoint - 0xe000,
334 0xd800 - 0x800 + 0x2000,
335 ),
336 4 => (
337 StructuralAlphabet::FourByte,
338 codepoint - 0x10000,
339 0x110000 - 0x10000,
340 ),
341 _ => unreachable!("Rust char values use one to four UTF-8 bytes"),
342 }
343}
344
345fn structural_character_from_rank(original: char, rank: u32) -> char {
346 let codepoint = match original.len_utf8() {
347 1 if (' '..='~').contains(&original) => 0x20 + rank,
348 1 if original < ' ' => rank,
349 1 => {
350 if rank < 0x20 {
351 rank
352 } else {
353 0x7f
354 }
355 }
356 2 => 0x80 + rank,
357 3 if rank < 0xd800 - 0x800 => 0x800 + rank,
358 3 => 0xe000 + rank - (0xd800 - 0x800),
359 4 => 0x10000 + rank,
360 _ => unreachable!("Rust char values use one to four UTF-8 bytes"),
361 };
362 char::from_u32(codepoint).expect("character rank should map to a valid Unicode scalar")
363}
364
365fn mapped_actor(actors: &HashMap<ActorId, ActorId>, actor: &ActorId) -> ActorId {
366 actors
367 .get(actor)
368 .expect("every referenced actor should be present in the change actor table")
369 .clone()
370}
371
372fn map_op_id(id: &mut OpId, actors: &HashMap<ActorId, ActorId>) {
373 id.1 = mapped_actor(actors, &id.1);
374}
375
376fn map_object_id(id: &mut ObjectId, actors: &HashMap<ActorId, ActorId>) {
377 if let ObjectId::Id(id) = id {
378 map_op_id(id, actors);
379 }
380}
381
382#[cfg(test)]
383mod fuzz;
384#[cfg(test)]
385mod shape;
386
387#[cfg(test)]
388mod tests {
389 use super::{shape::ShapeSignature, Anonymization, Key, OpType};
390 use crate::transaction::{CommitOptions, Transactable};
391 use crate::{AutoCommit, Automerge, ObjType, ReadDoc, ScalarValue, ROOT};
392 use std::collections::{HashMap, HashSet};
393
394 #[test]
395 fn repeated_numeric_values_receive_varied_replacements() {
396 let mut anonymization = Anonymization::from_seed([2; 32]);
397
398 let signed = (0..16)
399 .map(|_| anonymization.random_i64_other_than(42))
400 .collect::<HashSet<_>>();
401 let unsigned = (0..16)
402 .map(|_| anonymization.random_u64_other_than(42))
403 .collect::<HashSet<_>>();
404 let floats = (0..16)
405 .map(|_| anonymization.random_f64_other_than(42.0).to_bits())
406 .collect::<HashSet<_>>();
407
408 assert!(signed.len() > 1 && !signed.contains(&42));
409 assert!(unsigned.len() > 1 && !unsigned.contains(&42));
410 assert!(floats.len() > 1 && !floats.contains(&42.0_f64.to_bits()));
411 }
412
413 #[test]
414 fn structural_strings_are_seeded_unique_and_preserve_encoded_lengths() {
415 let mut first = Anonymization::from_seed([3; 32]);
416 let mut second = Anonymization::from_seed([4; 32]);
417 let source = ["name", "email", "a", "b", "é", "界", "😀"];
418 let anonymized = source.map(|value| first.anonymize_structural_string(value));
419 let differently_anonymized = source.map(|value| second.anonymize_structural_string(value));
420
421 assert_ne!(anonymized, differently_anonymized);
422 assert_eq!(
423 anonymized
424 .iter()
425 .collect::<std::collections::HashSet<_>>()
426 .len(),
427 source.len()
428 );
429 for (source, anonymized) in source.into_iter().zip(anonymized) {
430 assert_ne!(source, anonymized);
431 assert_eq!(source.len(), anonymized.len());
432 assert_eq!(
433 source.encode_utf16().count(),
434 anonymized.encode_utf16().count()
435 );
436 }
437 }
438
439 #[test]
440 fn anonymizes_data_while_preserving_history_and_shape() {
441 let mut source = AutoCommit::new();
442 source.put(ROOT, "private-key", "secret value").unwrap();
443 source.put(ROOT, "private-bytes", vec![1, 2, 3, 4]).unwrap();
444 source.put(ROOT, "private-number", 42_i64).unwrap();
445 source
446 .put(ROOT, "private-counter", ScalarValue::counter(10))
447 .unwrap();
448 let text = source
449 .put_object(ROOT, "private-text", ObjType::Text)
450 .unwrap();
451 source
452 .splice_text(&text, 0, 0, "Meeting with Alice 👋\nTomorrow")
453 .unwrap();
454 source.commit_with(
455 CommitOptions::default()
456 .with_message("private commit message")
457 .with_time(1_700_000_000),
458 );
459 source.increment(ROOT, "private-counter", 5).unwrap();
460 source.put(ROOT, "private-key", "another secret").unwrap();
461 source.commit();
462
463 let source = Automerge::load(&source.save()).unwrap();
464 let anonymized = Anonymization::from_seed([7; 32])
465 .anonymize(&source)
466 .unwrap();
467 let anonymized_again = Anonymization::from_seed([7; 32])
468 .anonymize(&source)
469 .unwrap();
470 let differently_anonymized = Anonymization::from_seed([8; 32])
471 .anonymize(&source)
472 .unwrap();
473
474 assert_eq!(anonymized.save(), anonymized_again.save());
475 assert_ne!(anonymized.save(), differently_anonymized.save());
476 assert_ne!(anonymized.save(), source.save());
477 Automerge::load(&anonymized.save()).unwrap();
478 assert_eq!(
479 ShapeSignature::new(&source),
480 ShapeSignature::new(&anonymized)
481 );
482
483 let source_changes = source.get_changes(&[]);
484 let anonymized_changes = anonymized.get_changes(&[]);
485 assert_eq!(source_changes.len(), anonymized_changes.len());
486 let source_indices = source_changes
487 .iter()
488 .enumerate()
489 .map(|(index, change)| (change.hash(), index))
490 .collect::<HashMap<_, _>>();
491 let anonymized_indices = anonymized_changes
492 .iter()
493 .enumerate()
494 .map(|(index, change)| (change.hash(), index))
495 .collect::<HashMap<_, _>>();
496 for (source, anonymized) in source_changes.iter().zip(&anonymized_changes) {
497 assert_eq!(source.len(), anonymized.len());
498 assert_eq!(source.seq(), anonymized.seq());
499 assert_eq!(source.start_op(), anonymized.start_op());
500 assert_ne!(source.actor_id(), anonymized.actor_id());
501 let source_deps = source
502 .deps()
503 .iter()
504 .map(|hash| source_indices[hash])
505 .collect::<Vec<_>>();
506 let anonymized_deps = anonymized
507 .deps()
508 .iter()
509 .map(|hash| anonymized_indices[hash])
510 .collect::<Vec<_>>();
511 assert_eq!(source_deps, anonymized_deps);
512 if source.timestamp() != 0 {
513 assert_ne!(source.timestamp(), anonymized.timestamp());
514 }
515 if source.message().is_some() {
516 assert_ne!(source.message(), anonymized.message());
517 }
518 }
519
520 assert!(anonymized.get(ROOT, "private-key").unwrap().is_none());
521 assert!(anonymized.get(ROOT, "private-text").unwrap().is_none());
522 let text_id = anonymized
523 .keys(ROOT)
524 .find_map(|key| match anonymized.get(ROOT, key).unwrap() {
525 Some((crate::Value::Object(ObjType::Text), object)) => Some(object),
526 _ => None,
527 })
528 .unwrap();
529 let source_text = source.text(&text).unwrap();
530 let anonymized_text = anonymized.text(&text_id).unwrap();
531 assert_ne!(source_text, anonymized_text);
532 assert_eq!(source_text.len(), anonymized_text.len());
533 assert_eq!(
534 source_text.encode_utf16().count(),
535 anonymized_text.encode_utf16().count()
536 );
537
538 for change in &anonymized_changes {
539 let expanded = change.decode();
540 assert_ne!(expanded.message.as_deref(), Some("private commit message"));
541 for operation in expanded.operations {
542 if let Key::Map(key) = operation.key {
543 assert!(!key.starts_with("private"));
544 }
545 match operation.action {
546 OpType::Put(ScalarValue::Str(value)) => {
547 assert_ne!(value.as_str(), "secret value");
548 assert_ne!(value.as_str(), "another secret");
549 }
550 OpType::Put(ScalarValue::Bytes(value)) => {
551 assert_ne!(value, vec![1, 2, 3, 4]);
552 }
553 _ => {}
554 }
555 }
556 }
557 }
558}