DEV Community

Cover image for JavaScript: How to decode the GreenPass QR code
Lorenzo Millucci
Lorenzo Millucci

Posted on • Edited on

JavaScript: How to decode the GreenPass QR code

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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Top comments (31)

Collapse
 
lgrauser profile image
lgrauser

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 ?

Collapse
 
lmillucci profile image
Lorenzo Millucci

Have you decoded data from Base45 format and have you decompressed it using zlib before using cbor?

Collapse
 
Sloan, the sloth mascot
Comment deleted
 
lmillucci profile image
Lorenzo Millucci

I've decoded your QR

{
  "v": [
    {
      "ci": "urn:uvci:....",
      "co": "FR",
      "dn": 2,
      "dt": "2021-07-01",
      "is": "CNAM",
      "ma": "...",
      "mp": "...",
      "sd": 2,
      "tg": "840539006",
      "vp": "...."
    }
  ],
  "dob": "1980-...",
  "nam": {
    "fn": "GR....",
    "gn": "LUD...",
    "fnt": "GR...",
    "gnt": "LUD..."
  },
  "ver": "1.3.0"
}
Enter fullscreen mode Exit fullscreen mode

PS: I suggest you to remove your data from the post above as anyone can decode it

Thread Thread
 
lgrauser profile image
lgrauser

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));

Thread Thread
 
lmillucci profile image
Lorenzo Millucci

you are using pako.deflate(decodedData) instead of const output = pako.inflate(decodedData);

Thread Thread
 
lgrauser profile image
lgrauser

oh yes . Sorry and once again thank you so much

Collapse
 
lgrauser profile image
lgrauser

with the upload

Collapse
 
lgrauser profile image
lgrauser

And the image :-)
Thank you so much again

Collapse
 
andrea_guido profile image
andrea guido

Hi Igrauser, Are you abe to help me in to create a valid gren pass from an existing valid one??
Thanks

Collapse
 
andrea_guido profile image
andrea guido

Igrauser can you help me with that?

Collapse
 
lucaarietti profile image
LucaArietti

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

Collapse
 
lmillucci profile image
Lorenzo Millucci

Non conosco nulla di specifico ma non dovrebbe essere difficilissimo da sviluppare.
Inoltre ho visto che nei centri commerciali esistono già totem del genere.

Collapse
 
xavfree profile image
xavfree
Collapse
 
lucaarietti profile image
LucaArietti

è esattamente ciò che cercavo!!!! Ed è open source! Grazie mille!!

Collapse
 
pipponee profile image
pipponee

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

Collapse
 
lmillucci profile image
Lorenzo Millucci

Node.js e JavaScript girano su qualsiasi OS. Quindi che sia Windows, Linux o Mac non fa differenza

Collapse
 
paolo_coppini_9aa6cd47faf profile image
Paolo Coppini

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?

Collapse
 
lmillucci profile image
Lorenzo Millucci

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

Collapse
 
krvngr profile image
krvngr

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

Collapse
 
sh4d0vv038 profile image
Sh4d0vv038 • Edited

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 ?

Collapse
 
mperu92 profile image
Matt P

you are my new hero.
Grazie!

Collapse
 
alby1974 profile image
alby1974

hi Matt are you able to help us to create a valid gren pass from an existing valid one??

Collapse
 
mperu92 profile image
Matt P • Edited

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!

Thread Thread
 
alby1974 profile image
alby1974 • Edited

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

Collapse
 
sonification profile image
sonification

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?

Collapse
 
lmillucci profile image
Lorenzo Millucci

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.

Collapse
 
simonh1000 profile image
Simon Hampton

very nice. I'd been trying without success.

Note that you can use the nodejs builtin instead on pako

zlib.inflateSync

Collapse
 
lmillucci profile image
Lorenzo Millucci

I will try. Thanks!

Collapse
 
soko372 profile image
Soko372

Can someone help me make a certificate? I'm an amateur and I have no idea