Skip to main content

automerge/
anonymize.rs

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/// An error encountered while anonymizing a document.
11#[derive(Debug, thiserror::Error)]
12pub enum AnonymizeError {
13    /// A change appeared before one of its dependencies.
14    #[error("change {change} appeared before dependency {dependency}")]
15    MissingDependency {
16        /// The change being anonymized.
17        change: ChangeHash,
18        /// The dependency which has not yet been anonymized.
19        dependency: ChangeHash,
20    },
21    /// The rewritten changes could not be applied.
22    #[error(transparent)]
23    Apply(#[from] AutomergeError),
24}
25
26/// Return a new document anonymized with a fresh random seed.
27pub 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            // Signed integer columns use delta encoding. Sampling from the i32 domain keeps the
187            // difference between any two generated values representable as an i64 while still
188            // producing independent, varied values for run-length and delta encoding.
189            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            // `rand` samples finite values from 0..1, avoiding NaNs and infinities while still
208            // producing independently encoded values for each occurrence.
209            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/// Lazily generated, seeded substitution tables for each character encoding class.
259///
260/// The rank conversion helpers below are deterministic, but they only index these tables. The
261/// source rank is mapped through a randomly shuffled derangement before being converted back into a
262/// character. This deliberately preserves character equality patterns and is consequently a
263/// substitution cipher, not cryptographic protection.
264#[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
313// Locate a character within an alphabet whose members have the same encoded width. Randomization
314// happens when `StructuralPermutations::replace` maps this rank through a seeded permutation.
315fn 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}