Skip to content

Instantly share code, notes, and snippets.

@willclarktech
Last active April 10, 2020 20:26
Show Gist options
  • Save willclarktech/b09fdf16b22e330c08613e7255327552 to your computer and use it in GitHub Desktop.
Save willclarktech/b09fdf16b22e330c08613e7255327552 to your computer and use it in GitHub Desktop.
/*
Example of an encrypted system in operation. This works with a few
assumptions that can be adjusted:
* Getting within approximately 70' is close enough to note
* "Infection" sticks around for 2 hours
Questions can be directed to TripleBlind, Inc. This code and algorithm
is donated to the Private Kit project.
Translated to TS from:
https://github.com/tripleblindmarket/covid-safe-paths/blob/develop/app/encryption/intersection.py
via:
https://gist.github.com/s0l0ist/59309573844e734398b4e9180935e555
*/
// npm i -D typescript @types/node @types/node-forge
// npm -i node-forge buffer
import * as forge from "node-forge";
import { Buffer } from "buffer";
import { strict as assert } from "assert";
type Location = readonly number[]; // [lat, long, time (probably in hours since app release?)]
type Thresholds = {
readonly distance: number; // ???
readonly time: number; // hours
};
// Not included in MessageDigest type definition for some reason
const SHA_256_DIGEST_LENGTH = 32;
const generateRandomNumberInclusive = (low: number, high: number) =>
Math.floor(Math.random() * (high - low + 1) + low);
const pbkdf2Hmac = (
data: readonly number[],
salt: string,
hashComplexity = 100000,
) => {
const rawData = Buffer.from(data);
const messageDigest = forge.md.sha256.create();
const derivedKey = forge.pkcs5.pbkdf2(
rawData.toString("binary"),
salt,
hashComplexity,
SHA_256_DIGEST_LENGTH,
messageDigest,
);
return Buffer.from(derivedKey, "binary").toString("hex");
};
const scaleThresholds = ({ distance, time }: Thresholds): Thresholds => ({
distance,
time: Math.floor(time / 2),
});
class InfectedUser {
public readonly salt: string;
constructor(salt = forge.random.getBytesSync(32)) {
this.salt = salt;
}
public generateTranslationVectorAndHash(
location: Location,
thresholds: Thresholds,
): { readonly translationVector: Location; readonly hash: string } {
const scaledThresholds = scaleThresholds(thresholds);
const template = InfectedUser.createTemplate(location, scaledThresholds);
const latticePoint = InfectedUser.generateRandomLatticePoint(
scaledThresholds,
);
const translationVector: Location = latticePoint.map(
(coordinate, i) => coordinate - template[i],
);
const hash = pbkdf2Hmac(latticePoint, this.salt);
return { translationVector, hash };
}
private static createTemplate(
[latitude, longitude, time]: Location,
{ time: timeThreshold }: Thresholds,
): Location {
return [
Math.floor(latitude * 10 ** 6),
Math.floor(longitude * 10 ** 6),
Math.floor(time + timeThreshold / 2),
];
}
private static generateRandomLatticePoint({
distance: distanceThreshold,
time: timeThreshold,
}: Thresholds): Location {
const randomX = generateRandomNumberInclusive(
Math.floor((-90 * 10 ** 6) / (2 * distanceThreshold)),
Math.floor((90 * 10 ** 6) / (2 * distanceThreshold)),
);
const randomY = generateRandomNumberInclusive(
Math.floor((-180 * 10 ** 6) / (2 * distanceThreshold)),
Math.floor((180 * 10 ** 6) / (2 * distanceThreshold)),
);
const randomZ = generateRandomNumberInclusive(0, 2 ** 50);
return [
randomX * 2 * distanceThreshold,
randomY * 2 * distanceThreshold,
randomZ * 2 * timeThreshold,
];
}
}
const scaleQuery = ([latitude, longitude, time]: Location): Location => [
Math.floor(latitude * 10 ** 6),
Math.floor(longitude * 10 ** 6),
Math.floor(time),
];
const translateQuery = (
translationVector: Location,
query: Location,
): Location => query.map((coordinate, i) => coordinate + translationVector[i]);
const quantize = (threshold: number, n: number): number =>
2 * threshold * Math.ceil((n - threshold) / (2 * threshold));
const quantizeQuery = (
{ distance: distanceThreshold, time: timeThreshold }: Thresholds,
query: Location,
): Location => [
quantize(distanceThreshold, query[0]),
quantize(distanceThreshold, query[1]),
quantize(timeThreshold, query[2]),
];
const generateHashForTranslationVector = (
query: Location,
translationVector: Location,
salt: string,
thresholds: Thresholds,
) => {
const scaledQuery = scaleQuery(query);
const translatedQuery = translateQuery(translationVector, scaledQuery);
const scaledThresholds = scaleThresholds(thresholds);
const quantizedQuery = quantizeQuery(scaledThresholds, translatedQuery);
return pbkdf2Hmac(quantizedQuery, salt);
};
const main = () => {
//TODO: More accurate threshold
// .000300 is approximately 70 feet
const thresholds: Thresholds = { distance: 300, time: 2 };
const infectedUser = new InfectedUser();
const infectedUserLocations: readonly Location[] = [
[41.40338, 39.289342, 32],
[2.192491, 145.293971, 55],
];
const {
translationVector,
hash: infectedHash,
} = infectedUser.generateTranslationVectorAndHash(
infectedUserLocations[0],
thresholds,
);
const testLocations: readonly Location[] = [
[41.40338, 39.289342, 32], // exact match
[41.40328, 39.289142, 33], // within threshold
[41.40328, 39.289142, 31], // before the infection
[41.40138, 39.289342, 31], // safe area
];
const testHashes = testLocations.map((location) =>
generateHashForTranslationVector(
location,
translationVector,
infectedUser.salt,
thresholds,
),
);
console.log(`${infectedHash} (Infected point hash)`);
console.log(
`${testHashes[0]} (This point is close to an infected point within 2 hours)`,
);
console.log(
`${testHashes[1]} (This point is close to an infected point within 2 hours)`,
);
console.log(`${testHashes[2]} (This point is safe)`);
console.log(`${testHashes[3]} (This point is safe)`);
assert.equal(
testHashes[0],
infectedHash,
`Expected hash ${testHashes[0]} for location ${testLocations[0]} to equal infected hash ${infectedHash}`,
);
assert.equal(
testHashes[1],
infectedHash,
`Expected hash ${testHashes[1]} for location ${testLocations[1]} to equal infected hash ${infectedHash}`,
);
assert.notEqual(
testHashes[2],
infectedHash,
`Expected hash ${testHashes[2]} for location ${testLocations[2]} not to equal infected hash ${infectedHash}`,
);
assert.notEqual(
testHashes[3],
infectedHash,
`Expected hash ${testHashes[3]} for location ${testLocations[3]} not to equal infected hash ${infectedHash}`,
);
};
main();
@willclarktech
Copy link
Author

Thanks, updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment