diff --git a/CovidQR-JS/covid-qr-decode.js b/CovidQR-JS/covid-qr-decode.js deleted file mode 100755 index 8432252..0000000 --- a/CovidQR-JS/covid-qr-decode.js +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env node -// -// Author: Guillaume Gagnon -// Licence: Apache 2.0 -// -// Extract all payloads included in the Covid QR provided by the Quebec government (Preuve/passeport de vaccination) -// Note: The public key do not seems to be provided by the government at this point. -// Hence, it is not possible to validate QR authenticity at the time being. -// Public keys should be available here later on: -// https://covid19.quebec.ca/PreuveVaccinaleApi/issuer/.well-known/jwks.json - -// This script has been built using this very nice and detailed HOWTO: -// https://github.com/dvci/health-cards-walkthrough/blob/main/SMART%20Health%20Cards.ipynb -// -// Also, more info about the SMART Health Cards Framework can be found here: -// https://smarthealth.cards/ - - -const fs = require('fs'); -var jsQR = require('jsqr'); -var PNG = require('pngjs').PNG; -var jose = require('node-jose'); -var base64url = require("base64url"); -var zlib = require("zlib"); - - -// Extract RAW QR from picture -imageData = PNG.sync.read(fs.readFileSync('./QR.png')) -const scannedQR = jsQR(new Uint8ClampedArray(imageData.data.buffer), imageData.width, imageData.height) -console.log("RAW QR DATA:") -console.log(scannedQR.data) -console.log("") - - -// Extract JWS -const scannedJWS = scannedQR - .chunks - .filter(chunk => chunk.type === "numeric")[0] // Grab the numeric chunk - .text.match(/(..?)/g) // Split into groups of 2 numeric characters each of which represent a single JWS char - .map(num => String.fromCharCode(parseInt(num, 10) + 45)).join('') // Convert from numeric encoding to JWS -console.log("JWS DATA:") -console.log(scannedJWS) -console.log("") - - -// Extract JWS Header -JWSHeaders = base64url.decode(scannedJWS.split(".")[0]) -console.log("JWS HEAD:") -console.log(JWSHeaders) -console.log("") - - -// Extract payload -JWSPayload = scannedJWS.split(".")[1] -const payload = Buffer.from(JWSPayload, "base64"); -zlib.inflateRaw(payload, function (err, decompressedResult) { - scannedResult = decompressedResult.toString("utf8"); - console.log(scannedResult) - //const entries = JSON.parse(scannedResult) // Uncomment this bloc if you want to "beautify" the json output - // .vc.credentialSubject.fhirBundle.entry - // .map(entry => console.log(JSON.stringify(entry, null, 2))) -}); diff --git a/CovidQR-JS/package-lock.json b/CovidQR-JS/package-lock.json deleted file mode 100644 index 38c57a5..0000000 --- a/CovidQR-JS/package-lock.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "name": "covid-qr-decode", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "jsqr": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz", - "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==" - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" - }, - "node-jose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.0.0.tgz", - "integrity": "sha512-j8zoFze1gijl8+DK/dSXXqX7+o2lMYv1XS+ptnXgGV/eloQaqq1YjNtieepbKs9jBS4WTnMOqyKSaQuunJzx0A==", - "requires": { - "base64url": "^3.0.1", - "buffer": "^5.5.0", - "es6-promise": "^4.2.8", - "lodash": "^4.17.15", - "long": "^4.0.0", - "node-forge": "^0.10.0", - "pako": "^1.0.11", - "process": "^0.11.10", - "uuid": "^3.3.3" - } - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "pngjs": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", - "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, - "zlib": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", - "integrity": "sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA=" - } - } -} diff --git a/CovidQR-JS/package.json b/CovidQR-JS/package.json deleted file mode 100644 index 9a32f57..0000000 --- a/CovidQR-JS/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "covid-qr-decode", - "version": "1.0.0", - "description": "", - "main": "covid-qr-decode.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "Guillaume Gagnon", - "license": "Apache-2.0", - "bin": { - "hello": "./covid-qr-decode.js" - }, - "dependencies": { - "base64url": "^3.0.1", - "jsqr": "^1.4.0", - "node-jose": "^2.0.0", - "pngjs": "^6.0.0", - "zlib": "^1.0.5" - } -} diff --git a/CovidQR-Py/covid-qr-decode.py b/CovidQR-Py/covid-qr-decode.py deleted file mode 100755 index cc5df50..0000000 --- a/CovidQR-Py/covid-qr-decode.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/python3 -# -# Author: Guillaume Gagnon -# Licence: Apache 2.0 -# -# Extract all payloads included in the Covid QR provided by the Quebec government (Preuve/passeport de vaccination) -# Note: The public key does not seem to be provided by the government at this point. -# Hence, it is not possible to validate QR authenticity at the time being. (Although code is provided) -# Public keys *should* be available here later on: -# https://covid19.quebec.ca/PreuveVaccinaleApi/issuer/.well-known/jwks.json - -# More info about the SMART Health Cards Framework can be found here: -# https://smarthealth.cards/ - -from pyzbar.pyzbar import decode # Need to: pip install pyzbar -from PIL import Image -import re -from jose import jwk # Need to: pip install python-jose -from jose.utils import base64url_decode -import zlib - -# Set path to the QR image -# TODO: Take as args? -QRImageFile = "./QR.png" - - -# Load QR data -decodedQR = decode(Image.open(QRImageFile)) -QRData = decodedQR[0].data.decode("utf-8") # Get first element (this library support multiple QR codes in a file) -print ("---- RAW QR DATA:") -print (QRData +" \n") - - -# Rebuild JWS token -QRNumericData = re.sub("[^0-9]", "", QRData) # Only keep numeric values -QRNumericPairs = re.findall("..", QRNumericData) # Split into groups of 2 numeric characters each of which represent a single JWS char -JWSToken = "" -for n in QRNumericPairs: - JWSToken += chr(int(n) + 45) # Recreate the JWS string -print ("---- JWS TOKEN:") -print (JWSToken +"\n") - - -# Extract JWS Content -header, payload, signature = JWSToken.rsplit('.') -print ("---- JWS HEADER:") -decHeader = base64url_decode(header.encode('utf-8')) -print (decHeader.decode('utf-8') +"\n") - -print ("---- JWS PAYLOAD:") -decPayload = base64url_decode(payload.encode('utf-8')) -uncompressedPayload = zlib.decompress(decPayload,-15) # Inflate RAW (use no headers bytes) -print (uncompressedPayload.decode("utf-8") + "\n") - -print ("---- JWS SIGNATURE (base64):") -print (signature + "\n") - - -# Verify JWS token signature -# Note: commented for now. Will be fully implemented once the public keys are provided by the government -#hmac_key = { -# "kid": "### INSERT KID FROM QR PAYLOAD HERE ###", -# "alg": "ES256", -# "kty": "EC", -# "crv": "P-256", -# "use": "sig", -# "x": "### INSERT X MATCHING KID HERE ###", -# "y": "### INSERT Y MATCHING KID HERE ###" -#} -#key = jwk.construct(hmac_key) -# -#signedMessage, encodedSignature = JWSToken.rsplit('.', 1) -#decoded_sig = base64url_decode(encodedSignature.encode('utf-8')) -#key.verify(signedMessage, decoded_sig) diff --git a/README.md b/README.md index 5286087..3e41080 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,33 @@ -# Covid Passport Decoder # +# Covid Passport Decoder -Extract all payloads included in the Covid QR provided by the Quebec government (Preuve/passeport de vaccination) +Forked from [ggqc007/Covid-QR-Decoder](https://github.com/ggqc007/Covid-QR-Decoder). Removed the JS version and improved the Py version. -**Note:** The public key does not seem to be provided by the government at this point. -Hence, it is not possible to validate QR authenticity at the time being. (Although some of the code is provided in the Python version) +* * * -Public keys *should* be available here later: -[https://covid19.quebec.ca/PreuveVaccinaleApi/issuer/.well-known/jwks.json](https://covid19.quebec.ca/PreuveVaccinaleApi/issuer/.well-known/jwks.json) +A simple commandline tool to extract the data from a Covid-19 QR code in the SMART Health Card format. Displays the data with fancy formatting. No error checking is preformed. + +Sample code is included to validate the QR code. Requires the public key from an authority (probably the government). More info about the SMART Health Cards Framework can be found here: -[https://smarthealth.cards/](https://smarthealth.cards/) + ---- +* * * -# This repo contains two versions # -## JavaScript: ## -1. cd CovidQR-JS/ -2. npm install -3. Edit the path to your .png QR image in the script -4. ./covid-qr-decode.js +# Usage +1. `pip install python-jose pyzbar rich` +2. `./covid-qr-decode.py [path to QR code]` -## Python: ## -1. cd CovidQR-Py/ -2. pip install python-jose pyzbar (this will install some dependencies) -3. Edit the path to your .png QR image in the script -4. ./covid-qr-decode.py +**Raw QR Code Data**: You can specify `--raw` to enter the raw QR code data instead of the path to a QR file. -# Sample payload # -> {"kid":"SOME-KEY-ID","zip":"SOME-ZIP","alg":"ES256"} +**Validate:** To validate the QR code signature, add `--validate [path to public key]`. ->{ +# Sample Data Output + +```json +{"kid":"SOME-KEY-ID","zip":"SOME-ZIP","alg":"ES256"} + +{ "resource": { "resourceType": "Patient", "name": [ @@ -86,6 +83,4 @@ More info about the SMART Health Cards Framework can be found here: ] } } - - - +``` diff --git a/covid-qr-decode.py b/covid-qr-decode.py new file mode 100755 index 0000000..6229551 --- /dev/null +++ b/covid-qr-decode.py @@ -0,0 +1,117 @@ +#!/usr/bin/python3 + +import argparse +import json +import os +import re +import sys +import zlib + +from jose import jwk +from jose.utils import base64url_decode +from PIL import Image +from pyzbar.pyzbar import decode +from rich import print_json +from rich.console import Console + +parser = argparse.ArgumentParser() +parser.add_argument('data', help='The path to the QR code image file.') +parser.add_argument('--raw', help='Decode raw QR code data instead of an image file.', action='store_true') +parser.add_argument('--validate', help='Validate the QR code signature against a public key. Must be the path to a public key.', nargs='?', default=False) + +# Print help if no input +if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1) + +args = parser.parse_args() + + +QRImageFile = args.data + +# Load data +if (not args.raw): + if (os.path.exists(QRImageFile)): + decodedQR = decode(Image.open(QRImageFile)) + # Check that the QR code was decoded + if (len(decodedQR) == 0): + print(f'\033[91mError:\033[00m could not read the QR code.') + sys.exit(1) + QRData = decodedQR[0].data.decode("utf-8") # Get first element (this library supports multiple QR codes in a file) + else: + print(f'\033[91mError:\033[00m could not find QR code file: {QRImageFile}') + sys.exit(1) +if (args.raw): + QRData = QRImageFile + +# Rebuild JWS token +QRNumericData = re.sub("[^0-9]", "", QRData) # Only keep numeric values +QRNumericPairs = re.findall("..", QRNumericData) # Split into groups of 2 numeric characters each of which represent a single JWS char +JWSToken = "" +for n in QRNumericPairs: + JWSToken += chr(int(n) + 45) # Recreate the JWS string + +# Extract JWS Content. +try: + header, payload, signature = JWSToken.rsplit('.') +except ValueError: + print(f'\033[91mError:\033[00m only found {len(JWSToken.rsplit("."))} out of 3 parts of the JWS token.') + + if (not args.raw): + print('The QR code couldn\'t be decoded.') + elif (args.raw): + print('The raw data couldn\'t be decoded. Make sure your provided string starts with \'shc:/\'.') + sys.exit(1) + +# Print +console = Console() + +if (not args.raw): + console.rule("[bold bright_red]RAW QR CODE DATA") + print(QRData + " \n") +elif (args.raw): + print('\n') # the raw qr code data can be confusing so lets put a line between that and the command above + +console.rule("[bold bright_red]JWS TOKEN") +print(JWSToken + "\n") + +console.rule("[bold bright_red]JWS HEADER") +decHeader = base64url_decode(header.encode('utf-8')) +print_json(decHeader.decode('utf-8')) +print('\n') + +console.rule("[bold bright_red]JWS SIGNATURE (base64)") +print(signature + "\n") + +console.rule("[bold bright_red]JWS PAYLOAD") +decPayload = base64url_decode(payload.encode('utf-8')) +uncompressedPayload = zlib.decompress(decPayload, -15).decode("utf-8") +print_json(uncompressedPayload) + +# Verify the JWS token signature +if (args.validate != False): + print('\n') + console.rule("[bold bright_red]Validate") + if (args.validate == None or len(args.validate) == 0): + print(f'\033[91mError:\033[00m must provide the path to the public key file (--validate [path to public key]) to validate the QR code signature against.') + else: + if (os.path.exists(args.validate)): + pass + else: + print(f'\033[91mError:\033[00m could not find public key file: {args.validate}') + print('Not implemented.') + +# hmac_key = { +# "kid": "### INSERT KID FROM QR PAYLOAD HERE ###", +# "alg": "ES256", +# "kty": "EC", +# "crv": "P-256", +# "use": "sig", +# "x": "### INSERT X MATCHING KID HERE ###", +# "y": "### INSERT Y MATCHING KID HERE ###" +# } +# key = jwk.construct(hmac_key) +# +# signedMessage, encodedSignature = JWSToken.rsplit('.', 1) +# decoded_sig = base64url_decode(encodedSignature.encode('utf-8')) +# key.verify(signedMessage, decoded_sig)