Copycat

repository·main·Indexed 22 days ago

https://github.com/supabase-community/copycat

A library for deterministic data anonymization that maps sensitive input values to realistic-looking replacement values using SipHash. It ensures the same input always produces the same output across different environments and processes, making it ideal for maintaining data relationships in anonymized datasets. Features include PII protection via secret hash keys, data scrambling, and generators for emails, UUIDs, phone numbers, and fictitious text.

Tokens
9.6K
Snippets
41
Records
42
Agent score
77%

What's inside @snaplet/copycat

  1. Protect PII using a Hash Key

    main

    Copycat uses SipHash to map inputs to outputs, making it computationally infeasible to infer the original input from the output. However, an attacker with access to the Copycat library can perform a brute-force attack by guessing inputs until they find a match.

    To prevent this, use a secret key with copycat.generateHashKey and apply it via copycat.setHashKey. This ensures that even if the library code is public, the mapping remains unknown to attackers without the key.

    // 1. Generate a key from a secret string and store it safely
    const key = copycat.generateHashKey('g9u*rT#!72R$zl5e')
    
    // 2. Apply the key to Copycat
    copycat.setHashKey(key)
    
    // Now mappings are unique to this key
    copycat.fullName('foo') // => 'Bertha Sauer'
  2. How Copycat works and why to use it

    main

    Copycat is a library designed for anonymizing sensitive data (PII) by replacing original values with replacement values that resemble the original but do not allow the original to be inferred.

    Unlike standard random generators (like faker) which provide a deterministic sequence of values, Copycat provides a deterministic mapping. This means for any given input, the same output is always returned regardless of the environment, process, or call order. This is achieved by hashing input values using SipHash, making it computationally infeasible to infer the original input from the output.

    Key characteristics:

    • Stateless: The output depends only on the input.
    • Deterministic Mapping: copycat.method(input) always yields the same result for the same input.
    • JSON-serializable inputs: Any JSON-serializable value can be used as input. For objects, property ordering is ignored (unlike JSON.stringify()).
    import { copycat } from '@snaplet/copycat'
    
    copycat.email('foo')
    // => 'Raleigh.McGlynn56687@wholewick.info'
    
    copycat.email('bar')
    // => 'Amir_Kris69246@raw-lout.name'
    
    copycat.email('foo')
    // => 'Raleigh.McGlynn56687@wholewick.info'
  3. Use Copycat for deterministic data anonymization

    main

    To use Copycat, import the copycat object from @snaplet/copycat. All methods follow the pattern copycat.method(input[, options]), where input is a JSON-serializable value. Because Copycat is stateless and uses SipHash under the hood, it ensures that the same input always maps to the same anonymized output, which is ideal for maintaining data relationships in anonymized datasets.

    import { copycat } from '@snaplet/copycat'
    
    // Example of deterministic mapping
    const email1 = copycat.email('user_123')
    const email2 = copycat.email('user_123')
    
    console.log(email1 === email2) // true
  4. Generate identity-like strings (email, uuid, url, etc.)

    main

    email(input[, options])

    • domain: Constrain to a specific domain.
    • limit: Max character length.

    uuid(input)

    Returns a string resembling a UUID.

    url(input[, options])

    • limit: Max character length.

    phoneNumber(input[, options])

    • length: Can be a number (exact) or { min, max }.
    • prefixes: Array of strings to use as prefixes (e.g., country codes).

    dateString(input[, options])

    Returns an ISO 8601 string.

    • minYear/maxYear: Year constraints.
    • min/max: Exact Date or string constraints.
    copycat.email('foo', { domain: 'acme.org' }) // => 'Albin_Goyette47922@acme.org'
    copycat.phoneNumber('foo', { prefixes: ['+33'], length: 11 }) // => '+3363998462'
    copycat.dateString('foo', { minYear: 2000, maxYear: 2010 }) // => '2005-03-15T14:10:10.000Z'
  5. Scramble input values with scramble()

    main

    Returns a value of the same type and length as the input, but with each character/digit replaced.

    • Strings: Replaces characters within the same range (e.g., lowercase stays lowercase, digits stay digits).
    • Numbers: Replaces digits and preserves floating point structure.
    • Objects/Arrays: Recursively scrambles values inside.
    • Dates: Scrambles each segment.
    • Booleans/Null: Returns the value as-is.
    • Other types: Throws an error.

    Options:

    • preserve: An array of characters that should remain unchanged (e.g., ['@', '.']).
    // String scrambling
    copycat.scramble('Zakary Hessel') // => 'Vqjmtp Rkbqyl'
    
    // Preserving specific characters
    copycat.scramble('foo@bar.org', { preserve: ['@', '.'] }) // => 'nzx@vib.elt'
    
    // Number scrambling
    copycat.scramble(782364.902374) // => 239724.505138
    
    // Recursive object scrambling
    copycat.scramble({ a: [{ b: 23, c: 'foo' }] }) // => { a: [{ b: 10, c: 'mem' }] }
  6. Generate a secure hash key with generateHashKey()

    main

    Takes a secret string and returns an array of four 32-bit integers (a Uint32Array). If the secret is not exactly 16 bytes, the key is derived from it. This key should be used with setHashKey to secure PII transformations.

    copycat.generateHashKey('Lhz1Xe7l$vPIwWr3')
    // => Uint32Array(4) [ 830105676, 1815569752, 1230009892, 863131511 ]
  7. Generate random numbers with int(), float(), and hex()

    main

    int(input[, options])

    Returns an integer.

    • min: Minimum value (default 0).
    • max: Maximum value (default Infinity).

    float(input[, options])

    Returns a number with both whole and decimal segments.

    • min: Minimum value.
    • max: Maximum value.

    hex(input[, options])

    Returns a string representing a hex value.

    • min: Minimum value.
    • max: Maximum value.
    copycat.int('foo', { min: 1, max: 10 }) // => 7
    copycat.float('foo', { min: 0, max: 1 }) // => 0.5782461953370469
    copycat.hex('foo') // => '6'
  8. Execute a function multiple times with times()

    main

    Takes an input and a function fn, calling fn repeatedly with a unique input for a number of times within the given range. Returns the results as an array.

    Range formats:

    • [min, max]: A tuple for a range of calls.
    • number: Exactly that many calls.
    // Range as tuple
    copycat.times('foo', [4, 5], copycat.word) // => [ 'Conspecta', 'Mihi', 'Fuisse', 'Philos', 'Divelistius' ]
    
    // Range as number
    copycat.times('foo', 2, copycat.word) // => [ 'Pugnari', 'Conspecta' ]
  9. Generate unique values with unique()

    main

    Tailored to maintain uniqueness of values after transformation. It attempts to generate a new value up to a specified number of attempts until a unique one is found using the provided store.

    Important Notes:

    • Not stateless: It relies on the store object to track values.
    • Determinism: Determinism is based on the combination of input, the store state, and the number of attempts.
    • Thread Safety: It is not thread-safe because it alters the global hashKey during attempts.
    • Duplicates: If the input contains duplicates, unique might hide them by generating different unique values for each. Use uniqueByInput to preserve input duplicates.
    const generateValue = (seed) => copycat.int(seed, { max: 3 });
    const store = new Set();
    
    copycat.unique('exampleSeed', generateValue, store); // => 3
    copycat.unique('exampleSeed1', generateValue, store); // => 1
    copycat.unique('exampleSeed', generateValue, store); // => 0
  10. Set the internal hash key with setHashKey()

    main

    Changes Copycat's internal state to use a specific key when mapping inputs to outputs. The key can be a string or the Uint32Array returned by copycat.generateHashKey().

    const key = copycat.generateHashKey('Lhz1Xe7l$vPIwWr3')
    copycat.setHashKey(key)
  11. Generate unique values while preserving input duplicates with uniqueByInput()

    main

    Designed to generate unique transformed values while ensuring that identical inputs always produce the same output.

    • Preserving Input Duplication: If the same input is provided multiple times, the output is consistently the same.
    • Uniqueness Preservation: For new inputs, it uses the unique logic to ensure the result hasn't been seen before in the resultStore.
    const method = (seed) => copycat.int(seed, { max: 3 });
    const resultStore = new Set();
    const inputStore = new Set();
    
    copycat.uniqueByInput('exampleSeed', method, inputStore, resultStore); // => 3
    copycat.uniqueByInput('exampleSeed1', method, inputStore, resultStore); // => 1
    copycat.uniqueByInput('exampleSeed', method, inputStore, resultStore); // => 3
  12. Generate fictitious text (words, sentences, paragraphs)

    main

    word(input[, options])

    • capitalize: Boolean or 'first' | 'all'.
    • minSyllables/maxSyllables: Syllable count constraints.

    words(input)

    Returns multiple words.

    • min/max: Number of words in the string.
    • capitalize: 'first' | 'all' | false.

    sentence(input[, options])

    • minClauses/maxClauses: Number of clauses.
    • minWords/maxWords: Words per clause.

    paragraph(input[, options])

    • minSentences/maxSentences: Number of sentences.
    • minClauses/maxClauses: Clauses per sentence.
    • minWords/maxWords: Words per clause.
    copycat.word('id-2', { minSyllables: 3, maxSyllables: 6 }) // => 'Nullam'
    copycat.words('foo', { min: 2, max: 3, capitalize: 'first' }) // => 'Aequo ophortatis'
    copycat.sentence('foo', { minWords: 5, maxWords: 8 }) // => 'Poetista graecis ne vel loque sic horum...'