improvements
This commit is contained in:
parent
7aea0de780
commit
5b724b3ee8
|
@ -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)))
|
||||
});
|
|
@ -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="
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
45
README.md
45
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/)
|
||||
<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:
|
|||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue