Last active
September 1, 2021 13:28
-
-
Save peakwinter/c820795563d54089280940e9b6da7073 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Decode and display information contained in a SMART Health Card (SHC) QR code | |
* Usage: node vaccineqr.js /path/to/qrcode.png | |
* | |
* Required dependencies: | |
* - jimp | |
* - jsqr | |
* | |
* 2021 Jacob Cook | |
*/ | |
const { promises: fs } = require("fs"); | |
const path = require("path"); | |
const jimp = require("jimp"); | |
const jsQR = require("jsqr"); | |
const zlib = require("zlib"); | |
const util = require("util"); | |
function jwtSectionToBuffer(section) { | |
return Buffer.from(section.replace("-", "+").replace("_", "/"), "base64"); | |
} | |
function parseJWTSection(section) { | |
return JSON.parse(jwtSectionToBuffer(section).toString("utf8")); | |
} | |
const zlibInflateRaw = util.promisify(zlib.inflateRaw); | |
/** | |
* Reads a QR code and returns its data as a string. | |
* @param {Buffer} buffer QR code image buffer in JPG/PNG/BMP format | |
* @returns {Promise<string>} | |
*/ | |
async function readQRCode(buffer) { | |
const processed_image = await jimp.read(buffer); | |
const qr = jsQR(processed_image.bitmap.data, processed_image.getWidth(), processed_image.getHeight()); | |
if (!qr) { | |
throw new Error("QR code could not be processed"); | |
} | |
return qr.data; | |
} | |
/** | |
* Parses a SMART Health Card's raw string representation into a signed JSON Web Token. | |
* @param {string} shc | |
* @returns {string} | |
*/ | |
function parseSHCtoJWT(shc) { | |
const data = shc.replace(/^shc:\//, "").replace(/[^0-9]/, ""); | |
const chars = []; | |
let index = 0; | |
while (index < data.length) { | |
const buf = Number(data.slice(index, index + 2)) + 45; | |
chars.push(String.fromCodePoint(buf)); | |
index += 2; | |
} | |
return Buffer.from(chars.join(""), "ascii").toString("utf8"); | |
} | |
/** | |
* Parses a JSON Web Token into its header, payload and signatures. | |
* @param {string} jwt | |
*/ | |
async function parseJWT(jwt) { | |
const [raw_header, raw_payload, signature] = jwt.split("."); | |
const header = parseJWTSection(raw_header); | |
let payload = raw_payload; | |
if (header.zip === "DEF") { | |
payload = await zlibInflateRaw(jwtSectionToBuffer(raw_payload)); | |
} | |
payload = JSON.parse(payload.toString("utf8")); | |
return { header, payload, signature }; | |
} | |
async function main() { | |
let image_path = process.argv[2]; | |
if (!image_path) { | |
throw new Error("No image path provided"); | |
} | |
if (image_path.startsWith(".")) { | |
image_path = path.join(__dirname, image_path); | |
} | |
const raw_image = await fs.readFile(image_path); | |
const shc = await readQRCode(raw_image); | |
const jwt = parseSHCtoJWT(shc); | |
const result = await parseJWT(jwt); | |
console.log(JSON.stringify(result)); | |
} | |
main() | |
.then(() => { | |
process.exit(0); | |
}) | |
.catch((err) => { | |
console.error(err); | |
process.exit(1); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment