The holidays are upon us and thanks to the advancement of vaccinations against the coronavirus pandemic it is finally possible to travel abroad.
In particular, starting from July 1st, it is be possible to travel freely within the borders of the European Union thanks to the release of the so-called "green pass".
But what is contained in the QR code that is sent to users? Thanks to the publication of all the specifications of the vaccination pass, I had some fun creating a script in JavaScript to read its contents.
But before explaining how I read the QR code of the green pass, let me introduce myself: I'm Lorenzo Millucci and I'm a software engineer who loves working with Symfony. You can read all my post on my blog (in Italian)
Reading the QR code
In order to create a script to decode the QR code of the green pass, the first thing to do is to prepare the environment by installing some dependencies:
npm install base45 cbor jpeg-js jsqr pako
At this point you are ready to import them into a script:
const base45 = require('base45');
const cbor = require('cbor');
const fs = require('fs')
const jpeg = require('jpeg-js');
const jsQR = require("jsqr");
const pako = require('pako');
Now you can start decoding the file containing the green pass. In this example I'm using the image file called greenpass.jpg
that I've downloaded directly from the Italian app IO
.
NOTE: if you used a different name or saved the file in another location, adjust the code accordingly.
const greenpassJpeg = fs.readFileSync(__ dirname + '/greenpass.jpg');
const greenpassImageData = jpeg.decode(greenpassJpeg, {useTArray: true});
NOTE 2: the useTArray
option passed to the decoder is used to make sure that the image is decoded as Uint8Array
Once this is done you can pass the file to the QR code decoder:
const decodedGreenpass = jsQR(greenpassImageData.data, greenpassImageData.width, greenpassImageData.height);
The string you get from this code is something like:
HC1: 6BFOXM% TS3DHPVO13J /G-/2YKVA.R/K86PP2FC1J9M$DI9C3 [....] CS62GMVR + B1YM K5MJ1K: K: 2JZLT6KM + DTVKPDUG $ E7F06FA3O6I-VA126Y0
To proceed with the decoding of the green pass you have to remove the first 4 characters of the string (which indicate the use of the HCERT protocol)
const greenpassBody = decodedGreenpass.data.substr(4);
At this point, in order to have the data in readable format, you must first decode the string from the Base45 format and then decompress it using zlib
:
const decodedData = base45.decode(greenpassBody);
const output = pako.inflate(decodedData);
As the certificate is encrypted using the COSE format (CBOR Object Signing and Encryption) you have to decrypt it:
const results = cbor.decodeAllSync(output);
[headers1, headers2, cbor_data, signature] = results[0].value;
The certificate contains various types of data useful to guarantee its validity but the part that contains the user's data is that contained in the variable cbor_data
const greenpassData = cbor.decodeAllSync(cbor_data);
At this point, finally, it is possible to print the JSON with the user data:
console.log (JSON.stringify(greenpassData[0].get(-260).get (1), null, 2));
For example this is the content of my green pass:
{
"t": [
{
"sc": "2021-06- []",
"but": "1606",
"tt": "LP217198-3",
"co": "IT",
"tc": "Dr. [....]",
"there": "[....]",
"is": "Ministry of Health",
"tg": "840539006",
"tr": "26041 [....]"
}
],
"nam": {
"fnt": "MILLUCCI",
"fn": "MILLUCCI",
"gnt": "LORENZO",
"gn": "LORENZO"
},
"ver": "1.0.0",
"dob": "1992-08-10"
}
Where:
-
sc
indicates the date and time of the test but it indicates "Marketing Authorization Holder" which simply indicates the body that put the test on the market -
tt
indicates the type of test -
tc
indicates the place where the test was performed -
ci
the unique certificate number (Unique Certificate Identifier or UVCI) -
is
the entity that issued the certificate -
tg
is the type of agent against which the vaccine acts (at the moment the only allowed value is 840539006 and that is COVID-19) -
tr
is the test result
To get all the details on the meaning of these acronyms you can read the official JSON Schema.
NOTE: my vaccination pass was obtained for having done the rapid test and therefore the data contained in it refers to an antigen test. The data contained in a green pass issued after a vaccine shot are different.
The complete script can be read below or can be found here
const base45 = require('base45');
const cbor = require('cbor');
const fs = require('fs')
const jpeg = require('jpeg-js');
const jsQR = require("jsqr");
const pako = require('pako');
// Set the path to the green pass QR
const FILE_PATH = __dirname + '/greenpass.jpg';
// Read image file
const greenpassJpeg = fs.readFileSync(FILE_PATH);
const greenpassImageData = jpeg.decode(greenpassJpeg, {useTArray: true});
// Decode QR
const decodedGreenpass = jsQR(greenpassImageData.data, greenpassImageData.width, greenpassImageData.height);
// Remove `HC1:` from the string
const greenpassBody = decodedGreenpass.data.substr(4);
// Data is Base45 encoded
const decodedData = base45.decode(greenpassBody);
// And zipped
const output = pako.inflate(decodedData);
const results = cbor.decodeAllSync(output);
[headers1, headers2, cbor_data, signature] = results[0].value;
const greenpassData = cbor.decodeAllSync(cbor_data);
console.log(JSON.stringify(greenpassData[0].get(-260).get(1), null, 2));
Top comments (31)
Hi Lorenzo,
Thanks for your post.
I was trying your code using the string contained in my UE QR CODE but when I used decodeAllSync I got the following error :
[ERR_ENCODING_INVALID_ENCODED_DATA]: The encoded data was not valid for encoding utf-8
Any idea of what could have went wrong ?
Have you decoded data from Base45 format and have you decompressed it using zlib before using cbor?
I've decoded your QR
PS: I suggest you to remove your data from the post above as anyone can decode it
Oh yes thank you. (gonna remove the QR data)
Here's my code
const greenpassJpeg = fs.readFileSync('qrcodelgr.jpg');
const greenpassImageData = jpeg.decode(greenpassJpeg, {useTArray: true});
const decodedGreenpass = jsQR (greenpassImageData.data, greenpassImageData.width, greenpassImageData.height);
console.log('pass decode');
console.log(decodedGreenpass.data);
const greenpassBody = decodedGreenpass.data.substr(4);
const decodedData = base45.decode(greenpassBody);
console.log('Post B45 decode');
console.log(decodedData);
const output = pako.deflate(decodedData);
console.log('apres zlib');
console.log(output);
const results = cbor.decodeAllSync(output);
consol.log('apres decodeallsync');
[headers1, headers2, cbor_data, signature] = results [0] .value;
const greenpassData = cbor.decodeAllSync(cbor_data);
console.log (JSON.stringify(greenpassData[0].get(-260).get (1), null, 2));
you are using
pako.deflate(decodedData)
instead ofconst output = pako.inflate(decodedData);
oh yes . Sorry and once again thank you so much
with the upload
And the image :-)
Thank you so much again
Hi Igrauser, Are you abe to help me in to create a valid gren pass from an existing valid one??
Thanks
Igrauser can you help me with that?
Ciao, che tu sappia ci sono app android (o siti web) che decodificano il qr code in maniera continuativa, da mettere su un tablet per fungere da totem verifica green pass?
Ho pensato di modificare la app verifica C19 per fare in modo che entri direttamente in modalità lettura qrcode e una volta decodificato, dopo 3 o 4 secondi ritorni in modalità lettura qr code....
Che tu sappia esiste? Sarebbe top per mettere appunto su un tablet e la gente così fa il self check in....
grazie
Non conosco nulla di specifico ma non dovrebbe essere difficilissimo da sviluppare.
Inoltre ho visto che nei centri commerciali esistono già totem del genere.
sanipasse.fr/
è esattamente ciò che cercavo!!!! Ed è open source! Grazie mille!!
ciao Lorenzo io sono nuovo sicuramente ti farro domande banali e incerte sono qui per capire e per apprendere sono un smanettone e mi piace imparare
Ti volevo chiedere per leggere decifrare il qr code utilizzi linuk kali
io ho iniziato per gioco attualmente lo utilizzo ma sono inesperto
volevo partire da zero per riuscire a decodificare il qrcode mi potresti aiutare
Node.js e JavaScript girano su qualsiasi OS. Quindi che sia Windows, Linux o Mac non fa differenza
Carissimo Lorenzo, qui ho bisogno di un ragguaglio: quel maledetto codice QR, compresso, criptato, codificato e Dio solo sa cos'altro, con la nuova applicazione che quei malefici al governo hanno pubblicato, nominata "VerificaC19", non ne va una dritta: tutti i codici che ho scansionato li dà come non validi, eppure sono originali ed emessi regolarmente dal Ministero, non sono qr farlocchi generati da chissà chi. Con un mio amico programmatore, perchè io di programmazione Java ne so molto poco, abbiamo provato a decodificare 3 codici qr, e i dati alla fine risultano tutti perfetti e veritieri. Cosa potrebbe esserci di sbagliato nei codici o nella applicazione? Unico particolare è che con VerificaC19 li dà non validi, con due altri normalissimi lettori invece riconosce tutti i dati da cima a fondo. Cosa dici? Che problema potrebbe esserci?
Ciao Paolo, sinceramente non ne ho idea. Se i codici sono stati rilasciati dal Ministero non ci dovrebbero essere questi problemi per cui ti consiglio di rivolgerti direttamente al loro supporto per capire cosa c'è che non va.
L'unica cosa che posso sospattare è che il fatto che i dati vengano letti ma l'app VerificaC19 non li riconosca come validi mi fa pensare ad un problema con la firma (ad esempio i per cui è stata calcolata la firma potrebbero essere diversi da quelli presenti sul certificato). Però non saprei come aiutarti a risolvere il problema
Hi, Lorenzo,
Thanks for your post!
Why can't I deflate the 'output' in this line:
const output = pako.inflate(decodedData);
back to 'decodedData'? I tried to following line:
const encodedData = pako.deflate(output);
but the 'encodedData' has a length of 408. The 'decodedData' has a length of 414...
Thanks
hi lorenzo, im trying to make one of my own that it only uses the text after the HC1:, like without the QR code, im still learning, this is my code:
const pako = require('pako');
const base45 = require('base45');
const zlib = require('zlib');
const encodedData = 'NCFOXN%TSMAHN-H.L8%38Q%T6$823S0IIYMJ 43%C0C9BQF2LVU.TMBX4K>
const decodedData = base45.decode(encodedData).toString('utf-8');
const output = pako.inflate(decodedData);
console.log(output);
the problem is that im always getting this error:
/home/sh4dow/node_modules/pako/lib/inflate.js:384
if (inflator.err) throw inflator.msg || msg[inflator.err];
^
unknown compression method
Can you help me please ?
you are my new hero.
Grazie!
hi Matt are you able to help us to create a valid gren pass from an existing valid one??
no sorry! I just implemented a feature that reads the QR code in a website and then decrypt the returned string form the QR.
Reading the documentation I saw that the official applications read an ID that is generated by the member states of the eu (which is changed after N hours) so I think that first of all you have to be able to get these IDs to make the greenpass valid.
That's all I know!
Hi Matt, thanks anyway. Where I can read the documentation? can you write me on telegram? If yes I write to you my nickname, thanks
Hello Lorenzo,
sorry to bother, but what format is the picture of the QR code? I mean, does it have to be just the code square with no borders?
jsQR tries to locate the QR code inside an image. If you provide an image containing ohe QR code only you maximize the chance of successfully decode it.
very nice. I'd been trying without success.
Note that you can use the nodejs builtin instead on pako
zlib.inflateSync
I will try. Thanks!
Can someone help me make a certificate? I'm an amateur and I have no idea