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

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

16