improvements

This commit is contained in:
Cyberes 2023-05-31 12:45:29 -06:00
parent 7aea0de780
commit 5b724b3ee8
Signed by: cyberes
GPG Key ID: 6B4A33836A9500FE
6 changed files with 137 additions and 281 deletions

View File

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

View File

@ -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="
}
}
}

View File

@ -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"
}
}

View File

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

View File

@ -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/)
<https://spec.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:
]
}
}
```

117
covid-qr-decode.py Executable file
View File

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