UNPKG

rappor

Version:

Javascript implementation of RAPPOR

375 lines (320 loc) 10.3 kB
/*jslint bitwise: true, node: true */ /*globals Uint8Array,Uint32Array, DataView, crypto */ /** * This code attempts to be a functionally equivalent javascript translation of * The python implementation of RAPPOR at https://github.com/google/rappor. */ /** * RAPPOR encoding parameters. * These affect privacy / anonymity. See paper for details. */ var Params = { num_bloombits: 16, // Number of bloom filter bits (k) num_hashes: 2, // Number of bloom filter hashes (h) num_cohorts: 64, // Number of cohorts (m) prob_p: 0.5, prob_q: 0.75, prob_f: 0.5 }; /** * An implementation of random and randint backed by the insecure * Math.random. Useful for testing, but should not be used with real data. */ var NativeRandom = function () { 'use strict'; }; NativeRandom.prototype.random = function () { 'use strict'; return Math.random(); }; /** * Get a random integer between a and b, inclusive. */ NativeRandom.prototype.randint = function (a, b) { 'use strict'; return Math.floor(Math.random() * (b + 1 - a)) + a; }; /** * An implementation of random and randint with a state buffer where values * recorded while in a known seed state are replayed for subsequent instances * of the same seed. */ var MemoizingRandom = function (state) { 'use strict'; this.seedval = 0; this.pos = 0; this.state = state; }; MemoizingRandom.prototype.randint = function (a, b) { 'use strict'; if (this.seedval) { if (!this.state[this.seedval]) { this.state[this.seedval] = []; } if (this.state[this.seedval].length <= this.pos) { this.state[this.seedval].push(Math.floor(Math.random() * (b + 1 - a)) + a); } this.pos += 1; return this.state[this.seedval][this.pos - 1]; } else { return Math.floor(Math.random() * (b + 1 - a)) + a; } }; MemoizingRandom.prototype.makeDeterministic = function (typedArray) { var bufferUtil = require('./bufferUtil'); if (this.seedval) { if (!this.state[this.seedval]) { this.state[this.seedval] = []; } if (this.state[this.seedval][this.pos]) { bufferUtil.fromHexString(this.state[this.seedval][this.pos], typedArray); } else { this.state[this.seedval].push(bufferUtil.toHexString(typedArray)); } this.pos += 1; } else { return typedArray; } }; MemoizingRandom.prototype.getstate = function () { return this.seedval; }; MemoizingRandom.prototype.setstate = function (state) { this.seedval = 0; this.pos = 0; }; MemoizingRandom.prototype.seed = function (seed) { this.seedval = seed; this.pos = 0; }; /** * Create a buffer of {num_bits} random bits, where each bit has probability * {prob_one} of being 1. */ var simpleRandom = function (prob_one, num_bits, rand) { 'use strict'; var state = { p: prob_one, n: num_bits, r: rand }; return function (state) { var r = new Uint8Array(Math.ceil(state.n / 8)), i = 0; for (i = 0; i < state.n; i += 1) { if (state.r.random() < state.p) { r[Math.floor(i / 8)] |= (1 << (i % 8)); } } return r.buffer; }.bind({}, state); }; /** * Random distribution provider. */ var SimpleRandomFunctions = function (params, rand) { 'use strict'; this.rand = rand || new NativeRandom(); this.num_bits = params.num_bloombits; this.cohort_rand_fn = this.rand.randint.bind(this.rand); this.f_gen = simpleRandom(params.prob_f, this.num_bits, rand); this.p_gen = simpleRandom(params.prob_p, this.num_bits, rand); this.q_gen = simpleRandom(params.prob_q, this.num_bits, rand); this.uniform_gen = simpleRandom(0.5, this.num_bits, rand); }; function bigendian_encode(val) { 'use strict'; var result = ""; for (var i = 24; i >= 0; i-=8) { var theByte = (val & (0xFF << i)) >> i; result = result.concat(String.fromCharCode(theByte)); } return result; } function get_bf_bit(input_word, cohort, hash_no, num_bloombits) { 'use strict'; // Encode the cohort and inpud_word in the same as the python rappor client // does, so that its hash_candidates.py will work for decoding our output. // Follow the RAPPOR python client as closely as possible, so that we can // debug against it as a reference implementation. var value = bigendian_encode(cohort) + input_word; var md5 = require('md5')(value); var b = md5.substr(2*hash_no,1); var a = md5.substr(1+ (2*hash_no),1); return parseInt(''+b+''+a,16) % num_bloombits; } /** * Create a buffer of {num_bits} random bits, where each bit has probability * {prob_one} of being 1. Uses 32 bit precision with cryptographically random * values backed by crypto.getRandom */ var randBits = function (prob_one, num_bits, rand) { 'use strict'; var state = { p: prob_one * 0xffffffff, n: Math.ceil(num_bits), r: rand }, crypto = require('crypto'); return function (state) { var randomness, output = new Uint8Array(Math.ceil(state.n / 8)), i = 0; if (crypto.getRandomValues) { // Browser. randomness = new Uint32Array(state.n); crypto.getRandomValues(randomness); } else if (crypto.randomBytes) { // Node. randomness = new Uint32Array(new Uint8Array( crypto.randomBytes(4 * state.n) ).buffer); } for (i = 0; i < state.n; i += 1) { if (randomness[i] < state.p) { output[Math.floor(i / 8)] |= (1 << (i % 8)); } } if (state.r.makeDeterministic) { state.r.makeDeterministic(output); } return output.buffer; }.bind({}, state); }; /** * Alternative Random distribution provider. */ var AdvancedRandomFunctions = function (params) { 'use strict'; // Note: does not support seeding or getstate/setstate var rand = new NativeRandom(); this.cohort_rand_fn = rand.randint.bind(rand); this.num_bits = params.num_bloombits; this.f_gen = randBits(params.prob_f, this.num_bits, rand); this.p_gen = randBits(params.prob_p, this.num_bits, rand); this.q_gen = randBits(params.prob_q, this.num_bits, rand); this.uniform_gen = randBits(0.5, this.num_bits, rand); }; /** * Alternate Random distribution provider which allows memoization of * values generated due to known seeds, so that they will be re-generated * in a deterministic way. */ var MemoizedRandomFunctions = function(params, state) { 'use strict'; this.rand = new MemoizingRandom(state); this.cohort_rand_fn = this.rand.randint.bind(this.rand); this.num_bits = params.num_bloombits; this.f_gen = randBits(params.prob_f, this.num_bits, this.rand); this.p_gen = randBits(params.prob_p, this.num_bits, this.rand); this.q_gen = randBits(params.prob_q, this.num_bits, this.rand); this.uniform_gen = randBits(0.5, this.num_bits, this.rand); }; /** * The encoder obfuscates values for a given user using the RAPPOR algorithm * @param {String} user_id user ID, for generating cohort. * @param {Params} RAPPOR Params Controlling privacy * @param {rand_funcs} Randomness, can be deterministic for testing. */ var Encoder = function (user_id, params, rand_funcs) { 'use strict'; this.params = params || Params; this.user_id = user_id; this.rand_funcs = rand_funcs || new AdvancedRandomFunctions(this.params); }; /** * Compute masks for rappor's Permanent Randomized Response * The i^th Bloom Filter bit B_i set to be B'_i equals * 1 with probability f/2 -- (*) -- f_bits * 0 with probaility f/2 * B_i with probaility 1-f -- (&) -- mask_indices set to 0 here, i.e. no mask * Output bit indices corresponding to (&) and bits 0/1 corresponding to (*) */ Encoder.prototype.get_rappor_masks = function (word) { 'use strict'; var assigned_cohort, f_bits, mask_indices, stored_state; if (this.params.flag_oneprr) { stored_state = this.rand_funcs.rand.getstate(); this.rand_funcs.rand.seed(this.user_id + word); } assigned_cohort = this.params.num_cohorts; while (assigned_cohort >= this.params.num_cohorts) { assigned_cohort = this.rand_funcs.cohort_rand_fn(0, this.params.num_cohorts - 1); } // Uniform bits for (*) f_bits = this.rand_funcs.uniform_gen(); // Mask indices are 1 with probability f. mask_indices = this.rand_funcs.f_gen(); if (this.params.flag_oneprr) { this.rand_funcs.rand.setstate(stored_state); } return { assigned_cohort: assigned_cohort, f_bits: f_bits, mask_indices: mask_indices }; }; /** * Computer rappor (Instantaneous Randomized Response). */ Encoder.prototype.encode = function (word) { 'use strict'; var bitwise = require('./bufferUtil'), masks = this.get_rappor_masks(word), bloom_bits_array = new Uint8Array(Math.ceil(this.params.num_bloombits / 8)), i, bit_to_set, prr, p_bits, q_bits, irr; for (i = 0; i < this.params.num_hashes; i += 1) { bit_to_set = get_bf_bit(word, masks.assigned_cohort, i, this.params.num_bloombits); bloom_bits_array[Math.floor(bit_to_set / 8)] |= (1 << (bit_to_set % 8)); } prr = bitwise.or( bitwise.and(masks.f_bits, masks.mask_indices), bitwise.and(bloom_bits_array.buffer, bitwise.not(masks.mask_indices)) ); // Compute instantaneous randomized response: // If PRR bit is set, output 1 with probability q // if PRR bit is not set, output 1 with probability p p_bits = this.rand_funcs.p_gen(); q_bits = this.rand_funcs.q_gen(); irr = bitwise.or( bitwise.and(p_bits, bitwise.not(prr)), bitwise.and(q_bits, prr) ); return { cohort: masks.assigned_cohort, irr: irr, value: String(masks.assigned_cohort) + ',' + bitwise.toHexString(irr), toString: this.toString.bind(this, masks.assigned_cohort, irr) }; }; /** * Generate a string format of a RAPPOR entry compatible with * the sum_bits aggregator. */ Encoder.prototype.toString = function (cohort, irr) { 'use strict'; var bitwise = require('./bufferUtil'), output; output = String(this.user_id); output += ","; output += String(cohort); output += ","; output += bitwise.toBinaryString(irr); return output; }; exports.Encoder = Encoder; exports.Params = Params; exports.SimpleRandomFunctions = SimpleRandomFunctions; exports.AdvancedRandomFunctions = AdvancedRandomFunctions; exports.MemoizedRandomFunctions = MemoizedRandomFunctions; exports.get_bf_bit = get_bf_bit;