Commit 8b0ff425 authored by Cypres TAC's avatar Cypres TAC
Browse files

javascript naming and comments

parent 9008c271
Pipeline #226566 failed with stage
in 1 minute and 45 seconds
# Proof of concept in Javascript for the implementation of Location Specific Part (LSP) encoding in accordance with the CLEA protocol
## Objectives
The QR code of a location/event, dynamic, which must be scanned at the entrance contains an URL ("deep link") structured by a prefix (for example for France: https://tac.gouv.fr/), followed by the 'location Specific Part' coded in base64. This directory gives an example of encoding in Java language of the 'location Specific Part' of the QR code according to the [protocol Cléa](https://hal.inria.fr/hal-03146022).
This Proof of Concept can be used as a basis for:
* Validating the Javascript implementation of the encryption algorithms
* To be used as an elementary brick to generate Qrcode using a web-browser
## Highlighted Dependencies
This javascript is dedicated to run on web-browser implementing the [Web Cryptography API](https://www.w3.org/TR/WebCryptoAPI/) recommended by W3C.
### Files description
* `clea.js`: LSP Encoding in base64 using parameters and time respecting the CLEA protocol.
An example of use of `clea.js` is done by :
* `index.html`: html page to be loaded on a web-browser
* `index.js`: javascript getting config values and calling periodically the qrcode generator
* `qrcode.min.js`: javascript to manage the display of the qrcode and the widget to input parameters
When the html page is loaded, the current qrcode is displayed and you can input new config values. Every 10 secondes, a new qrcode is generated.
### Web browser supported
validation list in progress:
* Firefox 86
* Chrome 89
var t_periodStart = get_ntp_utc(true);
var LTId = new Uint8Array(16);
var LTKey = new Uint8Array(32);
// RPG
var ct_periodStart;
var t_qrStart;
/*
* Copyright (C) Inria, 2021
*/
/*
global configuration
*/
var globalConf = {
t_periodStart: getNtpUtc(true),
LTKey: new Uint8Array(32),
LTId: new Uint8Array(16),
t_periodStart: getNtpUtc(true),
ct_periodStart: 0,
t_qrStart: 0
}
// Display debug information on console
var verbose = false;
async function clea_start_new_period(config) {
t_periodStart = get_ntp_utc(true);
/**
* Start a new period to generate a new LSP computing LTKey (Temporary location
* 256-bits secret key) and LTId (Temporary location public UUID)
*
* use globalConf (see above)
* @param {conf} config user configuration
* conf = {SK_L, PK_SA, PK_MCTA,
* staff, CRIexp, venueType, venueCategory1, venueCategory2,
* countryCode, periodDuration, locContactMsg}
*
* @return
*/
async function cleaStartNewPeriod(config) {
t_periodStart = getNtpUtc(true);
// Compute LTKey
var tmp = new Uint8Array(64);
......@@ -31,18 +55,29 @@ async function clea_start_new_period(config) {
);
LTId = new Uint8Array(await crypto.subtle.sign("HMAC", key, one), 0, 16); // HMAC-SHA256-128
return clea_renew_qrcode(config);
return cleaRenewLSP(config);
}
async function clea_renew_qrcode(config) {
/**
* Generate a new locationSpecificPart (LSP)
*
* use globalConf (see above)
* @param {conf} config user configuration
* conf = {SK_L, PK_SA, PK_MCTA,
* staff, CRIexp, venueType, venueCategory1, venueCategory2,
* countryCode, periodDuration, locContactMsg}
*
* @return {string} encoded LSP in Base64 format
*/
async function cleaRenewLSP(config) {
const CLEAR_HEADER_SIZE = 17;
const MSG_SIZE = 44;
const LOC_MSG_SIZE = 16;
const TAG_AND_KEY = 49;
t_qrStart = get_ntp_utc(false);
t_qrStart = getNtpUtc(false);
ct_periodStart = t_periodStart / 3600;
var header = new Uint8Array(CLEAR_HEADER_SIZE);
var msg = new Uint8Array(MSG_SIZE + (config.locContactMsg ? LOC_MSG_SIZE + TAG_AND_KEY : 0));
var loc_msg = new Uint8Array(LOC_MSG_SIZE);
......@@ -67,11 +102,11 @@ async function clea_renew_qrcode(config) {
msg.set(LTKey, 12);
if (config.locContactMsg) {
const phone = parse_bcd(config.locContactMsg.locationPhone, 8);
const phone = parseBcd(config.locContactMsg.locationPhone, 8);
loc_msg.set(phone, 0);
const pin = parse_bcd(config.locContactMsg.locationPin, 4);
const pin = parseBcd(config.locContactMsg.locationPin, 4);
loc_msg.set(pin, 8);
encrypted_loc_msg = await encrypt(new Uint8Array(0), loc_msg, config.PK_MCTA);
msg.set(new Uint8Array(encrypted_loc_msg), 44);
}
......@@ -82,6 +117,16 @@ async function clea_renew_qrcode(config) {
return btoa((Array.from(new Uint8Array(output))).map(ch => String.fromCharCode(ch)).join(''));
}
/**
* Encrypt, respecting CLEA protocol: | header | msg |
*
* @param {Uint8Array} header associated data
* @param {Uint8Array} msg message to encrypt
* @param {string} publicKey EC public key
*
* @return {Uint8Array} data encrypted in binary format (bytes array)
*/
async function encrypt(header, message, publicKey) {
// Step 1: Import the publicKey Q
var ECPubKey = await crypto.subtle.importKey(
......@@ -109,9 +154,9 @@ async function encrypt(header, message, publicKey) {
EcKeyPair.publicKey
);
C0_Q = ecdh_raw_pubkey_compressed(new Uint8Array(C0));
C0_Q = ecdhRawPubKeyCompressed(new Uint8Array(C0));
print_buf("C0", C0_Q);
if (verbose) printBuf("C0", C0_Q);
// Step 4: Generate a shared secret (S = rQ)
var S = await crypto.subtle.deriveBits({
......@@ -123,14 +168,14 @@ async function encrypt(header, message, publicKey) {
256
);
print_buf("S", S);
if (verbose) printBuf("S", S);
// Step5: Compute the AES encryption key K = KDF1(C0 | S)
var tmp = concatBuffer(concatBuffer(C0_Q, S), new ArrayBuffer(4));
var kdf1 = await crypto.subtle.digest("SHA-256", tmp)
print_buf("C0 | S | 0x00000000", tmp);
print_buf("KDF1", kdf1)
if (verbose) printBuf("C0 | S | 0x00000000", tmp);
if (verbose) printBuf("KDF1", kdf1)
var derivedKey = await crypto.subtle.importKey(
"raw",
......@@ -156,12 +201,32 @@ async function encrypt(header, message, publicKey) {
const out_msg = concatBuffer(header, encoded);
const output = concatBuffer(out_msg, C0_Q);
print_buf("output", output);
if (verbose) printBuf("output", output);
return output;
}
function get_ntp_utc(round) {
/**
* Compress an elliptic Curve Public key
*
* @param {Uint8Array/Array Buffer} ec_raw_pubkey public key
* @return {Uint8Array} public key compressed
*/
function ecdhRawPubKeyCompressed(ec_raw_pubkey) {
const u8full = new Uint8Array(ec_raw_pubkey)
const len = u8full.byteLength
const u8 = u8full.slice(0, 1 + len >>> 1) // drop `y`
u8[0] = 0x2 | (u8full[len - 1] & 0x01) // encode sign of `y` in first bit
return u8.buffer
}
/**
* Get the NTP/UTC format time in seconds
*
* @param {boolean} round hour rounded (multiple of 3600 sec) or not
* @return {integer} NTP/UTC format time in seconds
*/
function getNtpUtc(round) {
const ONE_HOUR_IN_MS = 3600000;
var t = Date.now();
......@@ -188,10 +253,9 @@ function get_ntp_utc(round) {
/**
* Creates a new Uint8Array based on two different ArrayBuffers
*
* @private
* @param {ArrayBuffers} buf1 The first buffer.
* @param {ArrayBuffers} buf2 The second buffer.
* @return {ArrayBuffers} The new ArrayBuffer created out of the two.
* @return {Uint8Array} The new buffer created out of the two.
*/
function concatBuffer(buf1, buf2) {
var out = new Uint8Array(buf1.byteLength + buf2.byteLength);
......@@ -200,40 +264,55 @@ function concatBuffer(buf1, buf2) {
return out.buffer;
}
function ecdh_raw_pubkey_compressed(ec_raw_pubkey) {
const u8full = new Uint8Array(ec_raw_pubkey)
const len = u8full.byteLength
const u8 = u8full.slice(0, 1 + len >>> 1) // drop `y`
u8[0] = 0x2 | (u8full[len - 1] & 0x01) // encode sign of `y` in first bit
return u8.buffer
}
function print_buf(name, b) {
console.log(name + " = " + Array.from(new Uint8Array(b)).map(u => ("0" + u.toString(16)).slice(-2)).join(""))
/**
* Display on console an array bytes contents
*
* @param {string} name name of the bytes array to be displayed
* @param {ArrayBuffers} buf array bytes
*/
function printBuf(name, buf) {
console.log(name + " = " + Array.from(new Uint8Array(buf)).map(u => ("0" + u.toString(16)).slice(-2)).join(""))
}
function getInt64Bytes(x) {
/**
* Convert a 34 bits int in a bytes array
*
* @param {integer} val to be converted
* @return {Uint8Array} bytes array
*/
function getInt64Bytes(val) {
var bytes = [];
var i = 8;
do {
bytes[--i] = x & (255);
x = x >> 8;
bytes[--i] = val & (255);
val = val >> 8;
} while (i)
return bytes;
}
function parse_bcd(string, size) {
var i = 0, ip, k;
/**
* Parse a string composed by digits ([0..9])
* to fill a bytes array storing as aset of
* 4-bit sub-fields that each contain a digit.
* padding is done by 0xF
*
* @param {string} string composed by digits ([0..9])
* @param {integer} size max number of digits to parse
* @return {Uint8Array} bytes array
*/
function parseBcd(string, size) {
var i = 0,
ip, k;
var array = new Uint8Array(size);
for (i = 0; i < string.length; i++) {
digit = string.charAt(i) - '0';
if (i % 2 == 0) {
ip = i / 2;
array[ip] = (digit << 4) | 0x0F;
} else {
} else {
ip = (i / 2) - 0.5;
array[ip] &= (0xF0 | digit);
array[ip] &= (0xF0 | digit);
}
}
......
......@@ -91,5 +91,5 @@
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="qrcode.min.js"></script>
<script src="clea.js"></script>
<script src="test.js"></script>
<script src="index.js"></script>
</html>
(() =>
{
var qrcode = new QRCode("qrcode", {width: 500, height: 500, correctLevel: QRCode.CorrectLevel.M});
/*
* Copyright (C) Inria, 2021
*/
(() => {
verbose = true
var qrcode = new QRCode("qrcode", {
width: 500,
height: 500,
correctLevel: QRCode.CorrectLevel.M
});
async function generate_qrcode()
{
/**
* Generate a QR code
* - generate the LSP using the clea.js function: cleaStartNewPeriod
* - generate the Qrcode adding the prefix for France http://tac.gouv.fr/ to LSP
*
*/
async function generateQrcode() {
var conf = {
SK_L: hexToBytes($("#sk_l").val()),
PK_SA: hexToBytes($("#pk_sa").val()),
......@@ -21,10 +33,8 @@
var phone = $("#locationPhone").val()
if(phone)
{
conf.locContactMsg =
{
if (phone) {
conf.locContactMsg = {
locationPhone: parseInt(phone),
locationPin: parseInt($("#locationPin").val())
}
......@@ -32,20 +42,26 @@
console.log(conf);
var b64 = await clea_start_new_period(conf);
var b64 = await cleaStartNewPeriod(conf);
qrcode.makeCode("http://tac.gouv.fr/" + b64);
}
// Convert a hex string to a byte array
function hexToBytes(hex)
{
/**
* Convert a hex string to a byte array
*
* @param {string} hex hexa string
* @return {bytes array}
*/
function hexToBytes(hex) {
var bytes = new Uint8Array(Math.ceil(hex.length / 2));
for (i = 0, c = 0; c < hex.length; i++, c += 2)
bytes[i] = parseInt(hex.substr(c, 2), 16);
return bytes;
}
generate_qrcode();
setInterval(generate_qrcode, 10000);
// Generate a Qr code when the page is loaded
generateQrcode();
// renew the Qrcode every 10 secondes
setInterval(generateQrcode, 10000);
})();
\ No newline at end of file
......@@ -15,16 +15,32 @@ This Proof of Concept can be used as a basis for:
### Files description
* `params_in.json`: a set of input parameters
* `encode_in.json`: a set of input parameters
* `test_clea.py`: test executable in Python
* `test_clea.html`: html page test for javascript CLEA
* `test_clea.js`: javascript embedded in `test_clea.html`
### C/Java
The test cycle for each set is as follows:
1. params_in.json -> [lsp_encode] -> lsp_out.json (private key + lsp in base64 format)
2. lsp_out.json -> [lsp_decode] -> params_out.json
3. test ok if the parameters in `params_in.json` encoded are identical to those decoded in `params_out.json` at the end of the chain
1. encode_in.json -> [lsp_encode] -> encode_out.json (private key + lsp in base64 format)
2. encode_out.json -> [lsp_decode] -> decode_out.json
3. test ok if the parameters in `encode_in.json` encoded are identical to those decoded in `de code_out.json` at the end of the chain
To launch the test cyle use `test_clea.py`:
```bash
python test_clea.py --help
usage: test_clea.py [-h] [--noencode] [--java]
optional arguments:
-h, --help show this help message and exit
--noencode test only the decoding part
--java encoding part with Java lib (C lib by default)
```
### Use
By default, the test cycle uses the C encoder (see below). The option `-java` allows to use the Java encoder.
```shell
>python3 test_clea.py
......@@ -36,3 +52,9 @@ TEST PASS: 1
TEST PASS: 2
ALL TESTS PASS
```
### Javascript
For using the javacript encoder in phase 1. of the test cycle, we need to load the page `test_clea.html`, load the json file `encode_in.json` (button browse...) and save the results in `encode_out.json` (button Write).
It is now possible to test the Java decoding to check interoperability using `python3 test_clea.py --noencode`. The option allows the skip the C or java encoding performed by your web-browser.
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Cléa tests</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Cléa test</h1>
<input type="file" id="fileinput" />
<textarea rows=20 style="width:900px" id="area"></textarea>
<input id="fileoutput" type="button" value="Write">
<script type="text/javascript">
function readSingleFile(evt) {
// File
var vFile = evt.target.files[0];
// Reader
var vReader = new FileReader();
vReader.readAsText(vFile);
vReader.onload = function(pEvent) {
// String Input
var vContent = pEvent.target.result;
// JSON to object
var vJson = JSON.parse(vContent);
// Query
var vResult = "";
generateLsps(vJson);
// Output
document.getElementById('area').value= vResult;
document.getElementById('fileoutput').disabled = false;
};
}
function download(filename, jsonObj) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(jsonObj, null, "\t")));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
document.getElementById('fileinput').addEventListener('change', readSingleFile, false);
document.getElementById('fileoutput').addEventListener("click", () => {
write_lsps();
download("encode_out.json",OUTPUT);
});
document.getElementById('fileoutput').disabled = true;
</script>
</body>
<script src="../js/clea.js"></script>
<script src="test_clea.js"></script>
</html>
\ No newline at end of file
/*
* Copyright (C) Inria, 2021
*/
var OUTPUT = [];
/**
* Generate a list of Location Specific Part (LSP)
*
* output results on global OUTPUT json format `encode_out.json`
*
* @param {json} conf_tests loaded from file `encode_in.json`
*/
async function generateLsps(conf_tests) {
var i = 0;
var vResult = "";
for (const conf of conf_tests) {
vResult += "Encode " + i + "\n";
conf.SK_L = hexToBytes(conf.SK_L);
conf.PK_SA = hexToBytes(conf.PK_SA);
conf.PK_MCTA = hexToBytes(conf.PK_MCTA);
if ((("locationPhone" in conf) == true) && (("locationPIN" in conf) == true)) {
conf['locContactMsg'] = {
locationPhone: conf.locationPhone,
locationPin: conf.locationPIN
}
}
var b64 = await cleaStartNewPeriod(conf);
vResult += b64 + "\n";
lsp_base_64 = {
lsp_base64: b64,
LTId: bytesToUuid(LTId),
ct_periodStart: ct_periodStart,
t_qrStart: t_qrStart,
SK_SA: conf.SK_SA,
SK_MCTA: conf.SK_MCTA,
}
OUTPUT.push(lsp_base_64);
i++;
}
document.getElementById('area').value = vResult;
}
/**
* Convert a hex string to a byte array
*
* @param {string} hex hexa string
* @return {bytes array}
*/
function hexToBytes(hex) {
var bytes = new Uint8Array(Math.ceil(hex.length / 2));
for (i = 0, c = 0; c < hex.length; i++, c += 2)
bytes[i] = parseInt(hex.substr(c, 2), 16);
return bytes;
}
/**
* Convert a bytes array to a hex string
*
* @param {bytes array} buffer
* @return {string} hexa string
*/
function bytesToHex(buffer) {
var bytes = new Uint8Array(buffer);
for (var hex = [], i = 0; i < bytes.length; i++) {
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
hex.push((current >>> 4).toString(16));
hex.push((current & 0xF).toString(16));
}
return hex.join("");
}
/**
* Convert a bytes array to UUID
*
* @param {bytes array} buffer of 32 bytes
* @return {string} UUUID format
*/
function bytesToUuid(buffer) {
const string = bytesToHex(buffer);
var uuid = string.substring(0, 8) + '-' + string.substring(8, 12) + '-' + string.substring(12, 16) + '-';
uuid += string.substring(16, 20) + '-' + string.substring(20, 32);
return uuid;
}
/**
* Write the json file
*
* global variable OUTPUT to be written
*
*/
async function write_lsps() {
console.log("OUT", OUTPUT);
}
\ No newline at end of file
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