Commit 6f5406da authored by Cypres TAC's avatar Cypres TAC
Browse files

Merge branch 'refine-js-api' into 'master'

Refine JS API

See merge request !43
parents 3865c2c3 9383f18d
Pipeline #251297 passed with stages
in 5 minutes and 32 seconds
......@@ -2,126 +2,132 @@
* Copyright (C) Inria, 2021
*/
/*
global configuration
*/
let gConf = {
LTKey: new Uint8Array(32),
LTId: new Uint8Array(16),
t_periodStart: getNtpUtc(true),
ct_periodStart: 0,
t_qrStart: 0
}
export const COUNTRY_SPECIFIC_PREFIX = "https://tac.gouv.fr?v=0#";
// Display debug information on console
let verbose = false;
/**
* Start a new period to generate a new LSP computing LTKey (Temporary location
* 256-bits secret key) and LTId (Temporary location public UUID)
*
* use gConf (see above)
* @param {conf} config user configuration
* conf = {SK_L, PK_SA, PK_MCTA,
* staff, CRIexp, venueType, venueCategory1, venueCategory2,
* periodDuration, locContactMsg}
*
* @return
*/
export async function cleaStartNewPeriod(config) {
gConf.t_periodStart = getNtpUtc(true);
// Compute LTKey
async function generateLocationTemporarySecretKey(SK_L, periodStartTime) {
let tmp = new Uint8Array(64);
tmp.set(config.SK_L, 0);
tmp[60] = (gConf.t_periodStart >> 24) & 0xFF;
tmp[61] = (gConf.t_periodStart >> 16) & 0xFF;
tmp[62] = (gConf.t_periodStart >> 8) & 0xFF;
tmp[63] = gConf.t_periodStart;
gConf.LTKey = await crypto.subtle.digest("SHA-256", tmp)
// Compute LTId
tmp.set(SK_L, 0);
tmp[60] = (periodStartTime >> 24) & 0xFF;
tmp[61] = (periodStartTime >> 16) & 0xFF;
tmp[62] = (periodStartTime >> 8) & 0xFF;
tmp[63] = periodStartTime;
return await crypto.subtle.digest("SHA-256", tmp);
}
async function generateLocationTemporaryId(LTKey) {
let one = new Uint8Array(1);
one[0] = 0x31; // '1'
let key = await crypto.subtle.importKey(
"raw",
gConf.LTKey, {
LTKey, {
name: "HMAC",
hash: "SHA-256"
},
true,
["sign"]
);
gConf.LTId = new Uint8Array(await crypto.subtle.sign("HMAC", key, one), 0, 16); // HMAC-SHA256-128
return new Uint8Array(await crypto.subtle.sign("HMAC", key, one), 0, 16); // HMAC-SHA256-128
}
return cleaRenewLSP(config);
export function newLocation(permanentLocationSecretKey, serverAuthorityPublicKey, manualContactTracingAuthorityPublicKey) {
return {
permanentLocationSecretKey,
serverAuthorityPublicKey,
manualContactTracingAuthorityPublicKey,
};
}
/**
* Generate a new locationSpecificPart (LSP)
*
* use gConf (see above)
* @param {conf} config user configuration
* conf = {SK_L, PK_SA, PK_MCTA,
* staff, CRIexp, venueType, venueCategory1, venueCategory2,
* periodDuration, locContactMsg}
*
* @return {string} encoded LSP in Base64 format
*/
export async function cleaRenewLSP(config) {
export async function newLocationSpecificPart(location, venueType, venueCategory1, venueCategory2, periodDuration, periodStartTime, qrCodeRenewalIntervalExponentCompact, qrCodeValidityStartTime) {
let locationTemporarySecretKey = await generateLocationTemporarySecretKey(location.permanentLocationSecretKey);
let locationTemporaryPublicId = await generateLocationTemporaryId(locationTemporarySecretKey);
return {
locationTemporarySecretKey,
locationTemporaryPublicId,
venueType,
venueCategory1,
venueCategory2,
periodDuration,
periodStartTime,
qrCodeRenewalIntervalExponentCompact,
qrCodeValidityStartTime,
};
}
export function renewLocationSpecificPart(locationSpecificPart, qrCodeValidityStartTime) {
locationSpecificPart.qrCodeValidityStartTime = qrCodeValidityStartTime;
}
export async function newDeepLink(location, locationSpecificPart, staff) {
let header = buildHeader(0, 0, locationSpecificPart.locationTemporaryPublicId); // FIXME version and qrType
let msg = await buildMessage(location, locationSpecificPart, staff);
let output = await encrypt(header, msg, location.serverAuthorityPublicKey);
// Convert output to Base64url
let base64url = btoa((Array.from(new Uint8Array(output))).map(ch => String.fromCharCode(ch)).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/={1,2}$/, '');
return COUNTRY_SPECIFIC_PREFIX + base64url;
}
export async function newDeepLinks(location, locationSpecificPart) {
let staffDeepLink = newDeepLink(location, locationSpecificPart, true);
let visitorsDeepLink = newDeepLink(location, locationSpecificPart, false);
{ staffDeepLink; visitorsDeepLink }
}
function buildHeader(version, qrType, locationTemporaryPublicId) {
const CLEAR_HEADER_SIZE = 17;
let header = new Uint8Array(CLEAR_HEADER_SIZE);
// Fill header
header[0] = ((version & 0x7) << 5) | ((qrType & 0x7) << 2);
header.set(locationTemporaryPublicId, 1);
return header;
}
async function buildMessage(location, locationSpecificPart, staff) {
const MSG_SIZE = 44;
const LOC_MSG_SIZE = 16;
const TAG_AND_KEY = 49;
gConf.t_qrStart = getNtpUtc(false);
gConf.ct_periodStart = gConf.t_periodStart / 3600;
let header = new Uint8Array(CLEAR_HEADER_SIZE);
let msg = new Uint8Array(MSG_SIZE + (config.locContactMsg ? LOC_MSG_SIZE + TAG_AND_KEY : 0));
let msg = new Uint8Array(MSG_SIZE + (locationSpecificPart.locContactMsg ? LOC_MSG_SIZE + TAG_AND_KEY : 0));
let loc_msg = new Uint8Array(LOC_MSG_SIZE);
// Fill header
header[0] = ((config.version & 0x7) << 5) | ((config.qrType & 0x7) << 2);
header.set(gConf.LTId, 1);
let compressedPeriodStartTime = locationSpecificPart.periodStartTime / 3600;
let qrCodeValidityStartTime = locationSpecificPart.qrCodeValidityStartTime;
// Fill message
let reserved = 0x0; // reserved for specification evolution
msg[0] = ((config.staff & 0x1) << 7) | (config.locContactMsg ? 0x40 : 0) | ((reserved & 0xFC0) >>> 6);
msg[1] = ((reserved & 0x3F) << 2) | ((config.CRIexp & 0x18) >> 3);
msg[2] = ((config.CRIexp & 0x7) << 5) | (config.venueType & 0x1F);
msg[3] = ((config.venueCategory1 & 0xF) << 4) | (config.venueCategory2 & 0xF);
msg[4] = config.periodDuration;
msg[5] = (gConf.ct_periodStart >> 16) & 0xFF; // multi-byte numbers are stored with the big endian convention as required by the specification
msg[6] = (gConf.ct_periodStart >> 8) & 0xFF;
msg[7] = gConf.ct_periodStart & 0xFF;
msg[8] = (gConf.t_qrStart >> 24) & 0xFF;
msg[9] = (gConf.t_qrStart >> 16) & 0xFF;
msg[10] = (gConf.t_qrStart >> 8) & 0xFF;
msg[11] = gConf.t_qrStart & 0xFF;
msg.set(new Uint8Array(gConf.LTKey), 12);
if (config.locContactMsg) {
const phone = parseBcd(config.locContactMsg.locationPhone, 8);
msg[0] = ((staff & 0x1) << 7) | (locationSpecificPart.locContactMsg ? 0x40 : 0) | ((reserved & 0xFC0) >>> 6);
msg[1] = ((reserved & 0x3F) << 2) | ((locationSpecificPart.qrCodeRenewalIntervalExponentCompact & 0x18) >> 3);
msg[2] = ((locationSpecificPart.qrCodeRenewalIntervalExponentCompact & 0x7) << 5) | (locationSpecificPart.venueType & 0x1F);
msg[3] = ((locationSpecificPart.venueCategory1 & 0xF) << 4) | (locationSpecificPart.venueCategory2 & 0xF);
msg[4] = locationSpecificPart.periodDuration;
msg[5] = (compressedPeriodStartTime >> 16) & 0xFF; // multi-byte numbers are stored with the big endian convention as required by the specification
msg[6] = (compressedPeriodStartTime >> 8) & 0xFF;
msg[7] = compressedPeriodStartTime & 0xFF;
msg[8] = (qrCodeValidityStartTime >> 24) & 0xFF;
msg[9] = (qrCodeValidityStartTime >> 16) & 0xFF;
msg[10] = (qrCodeValidityStartTime >> 8) & 0xFF;
msg[11] = qrCodeValidityStartTime & 0xFF;
msg.set(new Uint8Array(locationSpecificPart.locationTemporarySecretKey), 12);
if (locationSpecificPart.locContactMsg) {
const phone = parseBcd(locationSpecificPart.locContactMsg.locationPhone, 8);
loc_msg.set(phone, 0);
// Max digit is 15, the last 4 bits are set to 0 (pad)
loc_msg[7] = loc_msg[7] & 0xF0;
loc_msg[8] = config.locContactMsg.locationRegion & 0xFF;
const pin = parseBcd(config.locContactMsg.locationPin, 3);
loc_msg[8] = locationSpecificPart.locContactMsg.locationRegion & 0xFF;
const pin = parseBcd(locationSpecificPart.locContactMsg.locationPin, 3);
loc_msg.set(pin, 9);
let encrypted_loc_msg = await encrypt(new Uint8Array(0), loc_msg, config.PK_MCTA);
let encrypted_loc_msg = await encrypt(new Uint8Array(0), loc_msg, location.manualContactTracingAuthorityPublicKey);
msg.set(new Uint8Array(encrypted_loc_msg), 44);
}
let output = await encrypt(header, msg, config.PK_SA);
// Convert output to Base64
return btoa((Array.from(new Uint8Array(output))).map(ch => String.fromCharCode(ch)).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/={1,2}$/, '');
return msg;
}
/**
* Encrypt, respecting CLEA protocol: | header | msg |
*
......@@ -237,14 +243,6 @@ export function getNtpUtc(round) {
if (round) {
let th = Math.floor(t / ONE_HOUR_IN_MS); // Number of hours since the epoch
let rem = t % ONE_HOUR_IN_MS; // Number of ms since the last round hour
// Round the hour, i.e. if we are closer to the next round
// hour than the last one, round to the next hour
if (rem > ONE_HOUR_IN_MS / 2) {
th++;
}
t = th * 3600;
} else {
t /= 1000;
......
......@@ -11,7 +11,7 @@ function logEncodingDataAndResult(conf, result) {
let message = conf.SK_L_HEX+","
+conf.SK_MCTA_HEX+","
+conf.SK_SA_HEX+","
+result+","
+result.substring(clea.COUNTRY_SPECIFIC_PREFIX.length)+","
+conf.staff+","
+conf.CRIexp+","
+conf.venueType+","
......@@ -94,29 +94,38 @@ describe('getNtpUtc()', function () {
});
});
describe('cleaRenewLSP()', function () {
it('should return something with the right lenght and the right header', async () => {
let conf = configurationFromRun(runs[0]);
let result = await clea.cleaRenewLSP(conf);
logEncodingDataAndResult(conf, result);
expect([147, 234]).to.include(result.length);
expect(result.startsWith('AAAAAAAAAAAAAAAAAAAAAA')).to.be.true;
})
describe('renewLocationSpecificPart()', function () {
describe('test suite for renewLocationSpecificPart()', function () {
it('should return something with the right length and the right header', async () => {
let conf = configurationFromRun(runs[0]);
let location = await clea.newLocation(conf.SK_L, conf.PK_SA, conf.PK_MCTA);
let periodStartTime = clea.getNtpUtc(true);
let qrCodeValidityStartTime = clea.getNtpUtc(false);
let locationSpecificPart = await clea.newLocationSpecificPart(location, conf.venueType, conf.venueCategory1, conf.venueCategory2, conf.periodDuration, periodStartTime, conf.CRIexp, qrCodeValidityStartTime);
qrCodeValidityStartTime = clea.getNtpUtc(false);
await clea.renewLocationSpecificPart(locationSpecificPart, qrCodeValidityStartTime);
let result = await clea.newDeepLink(location, locationSpecificPart, conf.staff);
logEncodingDataAndResult(conf, result);
expect([147, 234]).to.include(result.length - clea.COUNTRY_SPECIFIC_PREFIX.length);
});
});
});
describe('cleaStartNewPeriod()', function () {
describe('test suite for cleaStartNewPeriod()', function () {
describe('newDeepLink()', function () {
describe('test suite for newDeepLink()', function () {
runs.forEach(function (run) {
it('should return a result with 147 length', async () => {
let conf = configurationFromRun(run);
let location = await clea.newLocation(conf.SK_L, conf.PK_SA, conf.PK_MCTA);
let result = await clea.cleaStartNewPeriod(conf);
let periodStartTime = clea.getNtpUtc(true);
let qrCodeValidityStartTime = clea.getNtpUtc(false);
let locationSpecificPart = await clea.newLocationSpecificPart(location, conf.venueType, conf.venueCategory1, conf.venueCategory2, conf.periodDuration, periodStartTime, conf.CRIexp, qrCodeValidityStartTime);
let result = await clea.newDeepLink(location, locationSpecificPart, conf.staff);
logEncodingDataAndResult(conf, result);
expect([147, 234]).to.include(result.length);
expect([147, 234]).to.include(result.length - clea.COUNTRY_SPECIFIC_PREFIX.length);
});
});
});
......@@ -214,4 +223,4 @@ describe('encrypt()', function () {
expect(resultInt8Array[i]).to.be.equal(header[i]);
}
});
})
});
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment