Last active
April 10, 2020 20:26
-
-
Save willclarktech/b09fdf16b22e330c08613e7255327552 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks, updated.