mirror of
https://github.com/covidpass-org/covidpass.git
synced 2025-02-23 06:57:40 +01:00
commit
8eac24880d
@ -117,16 +117,18 @@ function Form(): JSX.Element {
|
||||
|
||||
async function getPayload(file){
|
||||
try {
|
||||
const payload = await getPayloadBodyFromFile(file, COLORS.GREEN);
|
||||
const payload = await getPayloadBodyFromFile(file);
|
||||
setPayloadBody(payload);
|
||||
setFileLoading(false);
|
||||
setFile(file);
|
||||
|
||||
if (Object.keys(payload.receipts).length === 1) {
|
||||
setSelectedDose(parseInt(Object.keys(payload.receipts)[0]));
|
||||
}else{
|
||||
setShowDoseOption(true);
|
||||
}
|
||||
if (payload.rawData.length == 0) {
|
||||
if (Object.keys(payload.receipts).length === 1) {
|
||||
setSelectedDose(parseInt(Object.keys(payload.receipts)[0]));
|
||||
} else {
|
||||
setShowDoseOption(true);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setFile(file);
|
||||
setFileLoading(false);
|
||||
@ -253,21 +255,30 @@ function Form(): JSX.Element {
|
||||
|
||||
try {
|
||||
if (payloadBody) {
|
||||
const passName = payloadBody.receipts[selectedDose].name.replace(' ', '-');
|
||||
const vaxName = payloadBody.receipts[selectedDose].vaccineName.replace(' ', '-');
|
||||
const passDose = payloadBody.receipts[selectedDose].numDoses;
|
||||
|
||||
let selectedReceipt;
|
||||
if (payloadBody.rawData.length > 0) { // shc stuff
|
||||
const sortedKeys = Object.keys(payloadBody.receipts).sort(); // pickup the last key in the receipt table
|
||||
const lastKey = sortedKeys[sortedKeys.length - 1];
|
||||
selectedReceipt = payloadBody.receipts[lastKey];
|
||||
} else {
|
||||
selectedReceipt = payloadBody.receipts[selectedDose];
|
||||
}
|
||||
const passName = selectedReceipt.name.replace(' ', '-');
|
||||
const vaxName = selectedReceipt.vaccineName.replace(' ', '-');
|
||||
const passDose = selectedReceipt.numDoses;
|
||||
const covidPassFilename = `grassroots-receipt-${passName}-${vaxName}-${passDose}.pkpass`;
|
||||
|
||||
//console.log('> increment count');
|
||||
console.log('> increment count');
|
||||
await incrementCount();
|
||||
|
||||
// console.log('> generatePass');
|
||||
console.log('> generatePass');
|
||||
const pass = await PassData.generatePass(payloadBody, selectedDose);
|
||||
|
||||
//console.log('> create blob');
|
||||
console.log('> create blob');
|
||||
const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"});
|
||||
|
||||
//console.log(`> save blob as ${covidPassFilename}`);
|
||||
console.log(`> save blob as ${covidPassFilename}`);
|
||||
saveAs(passBlob, covidPassFilename);
|
||||
setSaveLoading(false);
|
||||
}
|
||||
@ -308,13 +319,24 @@ function Form(): JSX.Element {
|
||||
}
|
||||
|
||||
try {
|
||||
const passName = payloadBody.receipts[selectedDose].name.replace(' ', '-');
|
||||
const vaxName = payloadBody.receipts[selectedDose].vaccineName.replace(' ', '-');
|
||||
const passDose = payloadBody.receipts[selectedDose].numDoses;
|
||||
|
||||
let selectedReceipt;
|
||||
if (payloadBody.rawData.length > 0) { // shc stuff
|
||||
const sortedKeys = Object.keys(payloadBody.receipts).sort(); // pickup the last key in the receipt table
|
||||
const lastKey = sortedKeys[sortedKeys.length - 1];
|
||||
selectedReceipt = payloadBody.receipts[lastKey];
|
||||
setSelectedDose(Number(lastKey));
|
||||
} else {
|
||||
selectedReceipt = payloadBody.receipts[selectedDose];
|
||||
}
|
||||
const passName = selectedReceipt.name.replace(' ', '-');
|
||||
const vaxName = selectedReceipt.vaccineName.replace(' ', '-');
|
||||
const passDose = selectedReceipt.numDoses;
|
||||
const covidPassFilename = `grassroots-receipt-${passName}-${vaxName}-${passDose}.png`;
|
||||
|
||||
await incrementCount();
|
||||
let photoBlob = await Photo.generatePass(payloadBody, selectedDose);
|
||||
|
||||
let photoBlob = await Photo.generatePass(payloadBody, passDose);
|
||||
saveAs(photoBlob, covidPassFilename);
|
||||
|
||||
// need to clean up
|
||||
@ -420,7 +442,7 @@ function Form(): JSX.Element {
|
||||
|
||||
<input type='file'
|
||||
id='file'
|
||||
accept="application/pdf"
|
||||
accept="application/pdf,image/png"
|
||||
ref={inputFile}
|
||||
style={{display: 'none'}}
|
||||
/>
|
||||
|
@ -36,7 +36,7 @@ function Page(props: PageProps): JSX.Element {
|
||||
<a href="https://github.com/billylo1/covidpass" className="underline">{t('common:gitHub')}</a>
|
||||
<a href="https://vaccine-ontario.ca" className="underline">{t('common:returnToMainSite')}</a>
|
||||
</nav>
|
||||
<div className="flex pt-4 flex-row space-x-4 justify-center text-md flex-wrap">Last updated: 2021-09-29 (v1.9.12)</div>
|
||||
<div className="flex pt-4 flex-row space-x-4 justify-center text-md flex-wrap">Last updated: 2021-10-01 (v1.9.13)</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
@ -70,6 +70,14 @@ function Page(props: PageProps): JSX.Element {
|
||||
<td id='organization' style={{width: 220}}></td>
|
||||
<td id='vaccinationDate' style={{width:120}}></td>
|
||||
</tr>
|
||||
<tr id='extraRow2' hidden>
|
||||
<td id='organization2' style={{width: 220}}></td>
|
||||
<td id='vaccinationDate2' style={{width:120}}></td>
|
||||
</tr>
|
||||
<tr id='extraRow1' hidden>
|
||||
<td id='organization1' style={{width: 220}}></td>
|
||||
<td id='vaccinationDate1' style={{width:120}}></td>
|
||||
</tr>
|
||||
<tr style={{height: 20}}></tr>
|
||||
<tr>
|
||||
<td><b>NAME</b></td>
|
||||
@ -89,6 +97,7 @@ function Page(props: PageProps): JSX.Element {
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
<canvas id="canvas" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
82
package-lock.json
generated
82
package-lock.json
generated
@ -34,6 +34,7 @@
|
||||
"next-i18next": "^8.5.1",
|
||||
"next-seo": "^4.26.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-jose": "^2.0.0",
|
||||
"node-pdf-verifier": "^1.0.1",
|
||||
"pdfjs-dist": "^2.5.207",
|
||||
"pngjs": "^6.0.0",
|
||||
@ -911,6 +912,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/base64url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
@ -1732,6 +1741,11 @@
|
||||
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
|
||||
"integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw="
|
||||
},
|
||||
"node_modules/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=="
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@ -2709,6 +2723,11 @@
|
||||
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@ -3061,6 +3080,31 @@
|
||||
"he": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"dependencies": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/node-jose/node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/node-libs-browser": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
||||
@ -6000,6 +6044,11 @@
|
||||
"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=="
|
||||
},
|
||||
"big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
@ -6694,6 +6743,11 @@
|
||||
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
|
||||
"integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw="
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@ -7412,6 +7466,11 @@
|
||||
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
|
||||
"dev": true
|
||||
},
|
||||
"long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@ -7673,6 +7732,29 @@
|
||||
"he": "1.2.0"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-libs-browser": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
||||
|
@ -40,6 +40,7 @@
|
||||
"next-i18next": "^8.5.1",
|
||||
"next-seo": "^4.26.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-jose": "^2.0.0",
|
||||
"node-pdf-verifier": "^1.0.1",
|
||||
"pdfjs-dist": "^2.5.207",
|
||||
"pngjs": "^6.0.0",
|
||||
@ -54,6 +55,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pako": "^1.0.1",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
"@types/react": "^17.0.11",
|
||||
"autoprefixer": "^10.0.4",
|
||||
"postcss": "^8.1.10",
|
||||
|
@ -74,10 +74,11 @@ function Index(): JSX.Element {
|
||||
<Card content={
|
||||
<div><p>{t('common:subtitle')}</p><br /><p>{t('common:subtitle2')}</p><br />
|
||||
<b>{displayPassCount}</b><br/><br/>
|
||||
Sept 29 afternoon update:
|
||||
Oct 1 morning update:
|
||||
<br />
|
||||
<br />
|
||||
<ul className="list-decimal list-outside" style={{ marginLeft: '20px' }}>
|
||||
<li>Foundation improvements</li>
|
||||
<li>You can now select which page to import for multi-page receipts</li>
|
||||
<li>System reminders (e.g. unsupported browsers) are now on the top to improve ease of use</li>
|
||||
</ul><br />
|
||||
|
129
src/decode.ts
129
src/decode.ts
@ -1,48 +1,107 @@
|
||||
// Taken from https://github.com/ehn-dcc-development/ehn-sign-verify-javascript-trivial/blob/main/cose_verify.js
|
||||
// and https://github.com/ehn-dcc-development/dgc-check-mobile-app/blob/main/src/app/cose-js/sign.js
|
||||
// adapted from https://github.com/fproulx/shc-covid19-decoder/blob/main/src/shc.js
|
||||
|
||||
import base45 from 'base45';
|
||||
import pako from 'pako';
|
||||
import cbor from 'cbor-js';
|
||||
const jsQR = require("jsqr");
|
||||
const zlib = require("zlib");
|
||||
import {Receipt, HashTable} from "./payload";
|
||||
|
||||
export function typedArrayToBufferSliced(array: Uint8Array): ArrayBuffer {
|
||||
return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset);
|
||||
export function getQRFromImage(imageData) {
|
||||
return jsQR(
|
||||
new Uint8ClampedArray(imageData.data.buffer),
|
||||
imageData.width,
|
||||
imageData.height
|
||||
);
|
||||
}
|
||||
|
||||
export function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
||||
var buffer = new ArrayBuffer(array.length);
|
||||
// vaccine codes based on Alex Dunae's findings
|
||||
// https://gist.github.com/alexdunae/49cc0ea95001da3360ad6896fa5677ec
|
||||
// http://mchp-appserv.cpe.umanitoba.ca/viewConcept.php?printer=Y&conceptID=1514
|
||||
|
||||
array.map(function (value, i) {
|
||||
return buffer[i] = value;
|
||||
})
|
||||
// .vc.credentialSubject.fhirBundle.entry
|
||||
export function decodedStringToReceipt(decoded: object) : HashTable<Receipt> {
|
||||
|
||||
return array.buffer;
|
||||
}
|
||||
const codeToVaccineName = {
|
||||
'28581000087106': 'PFIZER',
|
||||
'28951000087107': 'JANSSEN',
|
||||
'28761000087108': 'ASTRAZENECA',
|
||||
'28571000087109': 'MODERNA'
|
||||
}
|
||||
|
||||
export function decodeData(data: string): Object {
|
||||
const cvxCodeToVaccineName = { // https://www2a.cdc.gov/vaccines/iis/iisstandards/vaccines.asp?rpt=cvx
|
||||
'208': 'PFIZER',
|
||||
'212': 'JANSSEN',
|
||||
'210': 'ASTRAZENECA',
|
||||
'207': 'MODERNA'
|
||||
}
|
||||
|
||||
if (data.startsWith('HC1')) {
|
||||
data = data.substring(3);
|
||||
// console.log(decoded);
|
||||
const shcResources = decoded['vc'].credentialSubject.fhirBundle.entry;
|
||||
let issuer;
|
||||
if (decoded['iss'].includes('quebec.ca')) {
|
||||
issuer = 'qc';
|
||||
}
|
||||
if (decoded['iss'].includes('ontariohealth.ca')) {
|
||||
issuer = 'on';
|
||||
}
|
||||
if (decoded['iss'] == "https://smarthealthcard.phsa.ca/v1/issuer") {
|
||||
issuer = 'bc';
|
||||
}
|
||||
|
||||
if (data.startsWith(':')) {
|
||||
data = data.substring(1);
|
||||
let name = '';
|
||||
let dateOfBirth;
|
||||
let receipts : HashTable<Receipt> = {};
|
||||
|
||||
const numResources = shcResources.length;
|
||||
for (let i = 0; i < numResources; i++) {
|
||||
const resource = shcResources[i]['resource'];
|
||||
if (resource["resourceType"] == 'Patient') {
|
||||
if (name.length > 0)
|
||||
name += '\n';
|
||||
|
||||
for (const nameField of resource.name) {
|
||||
for (const given of nameField.given) {
|
||||
name += (given + ' ')
|
||||
}
|
||||
name += (nameField.family);
|
||||
}
|
||||
dateOfBirth = resource['birthDate'];
|
||||
}
|
||||
if (resource["resourceType"] == 'Immunization') {
|
||||
let vaccineName : string;
|
||||
let organizationName : string;
|
||||
let vaccinationDate : string;
|
||||
for (const vaccineCodes of resource.vaccineCode.coding) {
|
||||
if (vaccineCodes.system.includes("snomed.info")) { //bc
|
||||
vaccineName = codeToVaccineName[vaccineCodes.code];
|
||||
if (vaccineName == undefined)
|
||||
vaccineName = 'Unknown - ' + vaccineCodes.code;
|
||||
} else if (vaccineCodes.system == "http://hl7.org/fhir/sid/cvx") { //qc
|
||||
vaccineName = cvxCodeToVaccineName[vaccineCodes.code];
|
||||
if (vaccineName == undefined)
|
||||
vaccineName = 'Unknown - ' + vaccineCodes.code;
|
||||
}
|
||||
}
|
||||
|
||||
let performers = resource['performer']; // BC
|
||||
let receiptNumber;
|
||||
if (issuer == 'bc') {
|
||||
performers = resource['performer'];
|
||||
receiptNumber = shcResources[i]['fullUrl'].split(':')[1];
|
||||
for (let j = 0; j < performers.length; j++) {
|
||||
const performer = performers[j];
|
||||
organizationName = performer.actor.display;
|
||||
}
|
||||
}
|
||||
if (issuer == 'qc') {
|
||||
organizationName = resource['location'].display; // QC
|
||||
receiptNumber = resource['protocolApplied'].doseNumber;
|
||||
}
|
||||
vaccinationDate = resource.occurrenceDateTime;
|
||||
|
||||
const receipt = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, receiptNumber, organizationName);
|
||||
// console.log(receipt);
|
||||
receipts[receiptNumber] = receipt;
|
||||
}
|
||||
}
|
||||
return receipts;
|
||||
|
||||
var arrayBuffer: Uint8Array = base45.decode(data);
|
||||
|
||||
if (arrayBuffer[0] == 0x78) {
|
||||
arrayBuffer = pako.inflate(arrayBuffer);
|
||||
}
|
||||
|
||||
var payloadArray: Array<Uint8Array> = cbor.decode(typedArrayToBuffer(arrayBuffer));
|
||||
|
||||
if (!Array.isArray(payloadArray) || payloadArray.length !== 4) {
|
||||
throw new Error('decodingFailed');
|
||||
}
|
||||
|
||||
var plaintext: Uint8Array = payloadArray[2];
|
||||
var decoded: Object = cbor.decode(typedArrayToBufferSliced(plaintext));
|
||||
|
||||
return decoded;
|
||||
}
|
76
src/issuers.js
Normal file
76
src/issuers.js
Normal file
@ -0,0 +1,76 @@
|
||||
const issuers = [
|
||||
{
|
||||
id: "ca.qc",
|
||||
iss: "https://covid19.quebec.ca/PreuveVaccinaleApi/issuer",
|
||||
keys: [
|
||||
{ kid: "qFdl0tDZK9JAWP6g9_cAv57c3KWxMKwvxCrRVSzcxvM",
|
||||
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
|
||||
x: "XSxuwW_VI_s6lAw6LAlL8N7REGzQd_zXeIVDHP_j_Do",
|
||||
y: "88-aI4WAEl4YmUpew40a9vq_w5OcFvsuaKMxJRLRLL0" },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "us.ca",
|
||||
iss: "https://myvaccinerecord.cdph.ca.gov/creds",
|
||||
keys: [
|
||||
{ kid: "7JvktUpf1_9NPwdM-70FJT3YdyTiSe2IvmVxxgDSRb0",
|
||||
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
|
||||
x: "3dQz5ZlbazChP3U7bdqShfF0fvSXLXD9WMa1kqqH6i4",
|
||||
y: "FV4AsWjc7ZmfhSiHsw2gjnDMKNLwNqi2jMLmJpiKWtE" },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "us.ny",
|
||||
iss: "https://ekeys.ny.gov/epass/doh/dvc/2021",
|
||||
keys: [
|
||||
{ kid: "9ENs36Gsu-GmkWIyIH9XCozU9BFhLeaXvwrT3B97Wok",
|
||||
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
|
||||
x: "--M0AedrNg31sHZGAs6qg7WU9LwnDCMWmd6KjiKfrZU",
|
||||
y: "rSf2dKerJFW3-oUNcvyrI2x39hV2EbazORZvh44ukjg" },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "us.la",
|
||||
iss: "https://healthcardcert.lawallet.com",
|
||||
keys: [
|
||||
{ kid: "UOvXbgzZj4zL-lt1uJVS_98NHQrQz48FTdqQyNEdaNE",
|
||||
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
|
||||
x: "n1PxhSk7DQj8ZBK3VIfwhlcN__QG357gbiTfZYt1gn8",
|
||||
y: "ZDGv5JYe4mCm75HCsHG8UkIyffr1wcZMwJjH8v5cGCc" },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "ca.yt",
|
||||
iss: "https://pvc.service.yukon.ca/issuer",
|
||||
keys: [
|
||||
{ kid: "UnHGY-iyCIr__dzyqcxUiApMwU9lfeXnzT2i5Eo7TvE",
|
||||
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
|
||||
x: "wCeT9rdLYTpOK52OK0-oRbwDxbljJdNiDuxPsPt_1go",
|
||||
y: "IgFPi1OrHtJWJGwPMvlueeHmULUKEpScgpQtoHNjX-Q" },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "ca.bc",
|
||||
iss: "https://smarthealthcard.phsa.ca/v1/issuer",
|
||||
keys: [
|
||||
{ kid: "XCqxdhhS7SWlPqihaUXovM_FjU65WeoBFGc_ppent0Q",
|
||||
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
|
||||
x: "xscSbZemoTx1qFzFo-j9VSnvAXdv9K-3DchzJvNnwrY",
|
||||
y: "jA5uS5bz8R2nxf_TU-0ZmXq6CKWZhAG1Y4icAx8a9CA" },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "ca.sk",
|
||||
iss: "https://skphr.prd.telushealthspace.com",
|
||||
keys: [
|
||||
{ kid: "xOqUO82bEz8APn_5wohZZvSK4Ui6pqWdSAv5BEhkes0",
|
||||
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
|
||||
x: "Hk4ktlNfoIIo7jp5I8cefp54Ils3TsKvKXw_E9CGIPE",
|
||||
y: "7hVieFGuHJeaNRCxVgKeVpoxDJevytgoCxqVZ6cfcdk" },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
issuers,
|
||||
};
|
86
src/pass.ts
86
src/pass.ts
@ -5,26 +5,10 @@ import {Constants} from "./constants";
|
||||
import {Payload, PayloadBody, PassDictionary} from "./payload";
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { QRCodeMatrixUtil } from '@zxing/library';
|
||||
import {QrCode,Encoding,PackageResult,QrFormat,PassPhotoCommon} from './passphoto-common';
|
||||
|
||||
const crypto = require('crypto')
|
||||
|
||||
enum QrFormat {
|
||||
PKBarcodeFormatQR = 'PKBarcodeFormatQR',
|
||||
PKBarcodeFormatPDF417 = 'PKBarcodeFormatPDF417'
|
||||
}
|
||||
|
||||
enum Encoding {
|
||||
utf8 = "utf-8",
|
||||
iso88591 = "iso-8859-1"
|
||||
}
|
||||
|
||||
interface QrCode {
|
||||
message: string;
|
||||
format: QrFormat;
|
||||
messageEncoding: Encoding;
|
||||
// altText: string;
|
||||
}
|
||||
|
||||
interface SignData {
|
||||
PassJsonHash: string;
|
||||
useBlackVersion: boolean;
|
||||
@ -85,67 +69,12 @@ export class PassData {
|
||||
|
||||
// Create Payload
|
||||
try {
|
||||
const payload: Payload = new Payload(payloadBody, numDose);
|
||||
|
||||
payload.serialNumber = uuid4();
|
||||
|
||||
// register record
|
||||
|
||||
const clonedReceipt = Object.assign({}, payload.receipt);
|
||||
delete clonedReceipt.name;
|
||||
delete clonedReceipt.dateOfBirth;
|
||||
clonedReceipt["serialNumber"] = payload.serialNumber;
|
||||
clonedReceipt["type"] = 'applewallet';
|
||||
|
||||
let requestOptions = {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(clonedReceipt) // body data type must match "Content-Type" header
|
||||
}
|
||||
|
||||
// console.log('registering ' + JSON.stringify(clonedReceipt, null, 2));
|
||||
const configResponse = await fetch('/api/config');
|
||||
|
||||
const configResponseJson = await configResponse.json();
|
||||
|
||||
const verifierHost = configResponseJson.verifierHost;
|
||||
const registrationHost = configResponseJson.registrationHost;
|
||||
let functionSuffix = configResponseJson.functionSuffix;
|
||||
|
||||
if (functionSuffix == undefined)
|
||||
functionSuffix = '';
|
||||
|
||||
const registerUrl = `${registrationHost}/register${functionSuffix}`;
|
||||
// console.log(registerUrl);
|
||||
|
||||
const response = await fetch(registerUrl, requestOptions);
|
||||
const responseJson = await response.json();
|
||||
|
||||
console.log(JSON.stringify(responseJson,null,2));
|
||||
|
||||
if (responseJson["result"] != 'OK') {
|
||||
console.error(responseJson);
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const encodedUri = `serialNumber=${encodeURIComponent(payload.serialNumber)}&vaccineName=${encodeURIComponent(payload.receipt.vaccineName)}&vaccinationDate=${encodeURIComponent(payload.receipt.vaccinationDate)}&organization=${encodeURIComponent(payload.receipt.organization)}&dose=${encodeURIComponent(payload.receipt.numDoses)}`;
|
||||
const qrCodeUrl = `${verifierHost}/verify?${encodedUri}`;
|
||||
|
||||
// console.log(qrCodeUrl);
|
||||
|
||||
// Create QR Code Object
|
||||
const qrCode: QrCode = {
|
||||
message: qrCodeUrl,
|
||||
format: QrFormat.PKBarcodeFormatQR,
|
||||
messageEncoding: Encoding.iso88591,
|
||||
// altText : payload.rawData
|
||||
|
||||
}
|
||||
|
||||
|
||||
const results = await PassPhotoCommon.preparePayload(payloadBody, numDose);
|
||||
const payload = results.payload;
|
||||
// Create pass data
|
||||
const pass: PassData = new PassData(payload, qrCode);
|
||||
|
||||
const pass: PassData = new PassData(results.payload, results.qrCode);
|
||||
|
||||
// Create new zip
|
||||
const zip = [] as { path: string; data: Buffer | string }[];
|
||||
@ -197,8 +126,7 @@ export class PassData {
|
||||
|
||||
return createZip(zip);
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return Promise.reject();
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
109
src/passphoto-common.ts
Normal file
109
src/passphoto-common.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import {toBuffer as createZip} from 'do-not-zip';
|
||||
import {v4 as uuid4} from 'uuid';
|
||||
|
||||
import {Constants} from "./constants";
|
||||
import {Payload, PayloadBody, PassDictionary} from "./payload";
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { QRCodeMatrixUtil } from '@zxing/library';
|
||||
|
||||
export enum QrFormat {
|
||||
PKBarcodeFormatQR = 'PKBarcodeFormatQR',
|
||||
PKBarcodeFormatPDF417 = 'PKBarcodeFormatPDF417'
|
||||
}
|
||||
|
||||
export enum Encoding {
|
||||
utf8 = "utf-8",
|
||||
iso88591 = "iso-8859-1"
|
||||
}
|
||||
|
||||
export interface QrCode {
|
||||
message: string;
|
||||
format: QrFormat;
|
||||
messageEncoding: Encoding;
|
||||
// altText: string;
|
||||
}
|
||||
|
||||
export interface PackageResult {
|
||||
payload: Payload;
|
||||
qrCode: QrCode;
|
||||
}
|
||||
|
||||
export class PassPhotoCommon {
|
||||
|
||||
static async preparePayload(payloadBody: PayloadBody, numDose: number) : Promise<PackageResult> {
|
||||
|
||||
console.log('preparePayload');
|
||||
|
||||
// console.log(JSON.stringify(payloadBody, null, 2), numDose);
|
||||
|
||||
const payload: Payload = new Payload(payloadBody, numDose);
|
||||
|
||||
payload.serialNumber = uuid4();
|
||||
let qrCodeMessage;
|
||||
|
||||
if (payloadBody.rawData.startsWith('shc:/')) {
|
||||
|
||||
qrCodeMessage = payloadBody.rawData;
|
||||
|
||||
} else {
|
||||
|
||||
// register record
|
||||
|
||||
const clonedReceipt = Object.assign({}, payloadBody.receipts[numDose]);
|
||||
delete clonedReceipt.name;
|
||||
delete clonedReceipt.dateOfBirth;
|
||||
clonedReceipt["serialNumber"] = payload.serialNumber;
|
||||
clonedReceipt["type"] = 'applewallet';
|
||||
|
||||
let requestOptions = {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(clonedReceipt) // body data type must match "Content-Type" header
|
||||
}
|
||||
|
||||
// console.log('registering ' + JSON.stringify(clonedReceipt, null, 2));
|
||||
const configResponse = await fetch('/api/config');
|
||||
|
||||
const configResponseJson = await configResponse.json();
|
||||
|
||||
const verifierHost = configResponseJson.verifierHost;
|
||||
const registrationHost = configResponseJson.registrationHost;
|
||||
let functionSuffix = configResponseJson.functionSuffix;
|
||||
|
||||
if (functionSuffix == undefined)
|
||||
functionSuffix = '';
|
||||
|
||||
const registerUrl = `${registrationHost}/register${functionSuffix}`;
|
||||
// console.log(registerUrl);
|
||||
|
||||
const response = await fetch(registerUrl, requestOptions);
|
||||
const responseJson = await response.json();
|
||||
|
||||
// console.log(JSON.stringify(responseJson,null,2));
|
||||
|
||||
if (responseJson["result"] != 'OK') {
|
||||
console.error(responseJson);
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const encodedUri = `serialNumber=${encodeURIComponent(payload.serialNumber)}&vaccineName=${encodeURIComponent(payloadBody.receipts[numDose].vaccineName)}&vaccinationDate=${encodeURIComponent(payloadBody.receipts[numDose].vaccinationDate)}&organization=${encodeURIComponent(payloadBody.receipts[numDose].organization)}&dose=${encodeURIComponent(payloadBody.receipts[numDose].numDoses)}`;
|
||||
const qrCodeUrl = `${verifierHost}/verify?${encodedUri}`;
|
||||
qrCodeMessage = qrCodeUrl;
|
||||
// console.log(qrCodeUrl);
|
||||
}
|
||||
|
||||
// Create QR Code Object
|
||||
const qrCode: QrCode = {
|
||||
message: qrCodeMessage,
|
||||
format: QrFormat.PKBarcodeFormatQR,
|
||||
messageEncoding: Encoding.iso88591,
|
||||
// altText : payload.rawData
|
||||
|
||||
}
|
||||
|
||||
return {payload: payload, qrCode: qrCode}
|
||||
|
||||
}
|
||||
}
|
148
src/payload.ts
148
src/payload.ts
@ -1,5 +1,6 @@
|
||||
import {Constants} from "./constants";
|
||||
import {COLORS} from "./colors";
|
||||
import { TEXT_ALIGN } from "html2canvas/dist/types/css/property-descriptors/text-align";
|
||||
|
||||
export class Receipt {
|
||||
constructor(public name: string, public vaccinationDate: string, public vaccineName: string, public dateOfBirth: string, public numDoses: number, public organization: string) {};
|
||||
@ -36,7 +37,7 @@ export interface PayloadBody {
|
||||
|
||||
export class Payload {
|
||||
|
||||
receipt: Receipt;
|
||||
receipts: HashTable<Receipt>;
|
||||
rawData: string;
|
||||
backgroundColor: string;
|
||||
labelColor: string;
|
||||
@ -48,10 +49,51 @@ export class Payload {
|
||||
|
||||
constructor(body: PayloadBody, numDose: number) {
|
||||
|
||||
// Get name and date of birth information
|
||||
const name = body.receipts[numDose].name;
|
||||
const dateOfBirth = body.receipts[numDose].dateOfBirth;
|
||||
const vaccineName = body.receipts[numDose].vaccineName;
|
||||
let generic: PassDictionary = {
|
||||
headerFields: [],
|
||||
primaryFields: [],
|
||||
secondaryFields: [],
|
||||
auxiliaryFields: [],
|
||||
backFields: []
|
||||
}
|
||||
this.backgroundColor = COLORS.YELLOW;
|
||||
this.labelColor = COLORS.WHITE
|
||||
this.foregroundColor = COLORS.WHITE
|
||||
this.img1x = Constants.img1xWhite
|
||||
this.img2x = Constants.img2xWhite
|
||||
|
||||
let fullyVaccinated = false;
|
||||
var keys = Object.keys(body.receipts).reverse();
|
||||
|
||||
if (body.rawData.length > 0) { // SHC contains multiple receipts
|
||||
for (let k of keys) {
|
||||
fullyVaccinated = processReceipt(body.receipts[k], generic);
|
||||
if (fullyVaccinated) {
|
||||
this.backgroundColor = COLORS.GREEN;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fullyVaccinated = processReceipt(body.receipts[numDose], generic);
|
||||
if (fullyVaccinated) {
|
||||
this.backgroundColor = COLORS.GREEN;
|
||||
}
|
||||
}
|
||||
|
||||
this.receipts = body.receipts;
|
||||
this.rawData = body.rawData;
|
||||
this.generic = generic;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function processReceipt(receipt: Receipt, generic: PassDictionary) : boolean {
|
||||
|
||||
console.log('processing receipt #' + receipt.numDoses);
|
||||
|
||||
const name = receipt['name'];
|
||||
const dateOfBirth = receipt.dateOfBirth;
|
||||
const numDoses = receipt.numDoses;
|
||||
const vaccineName = receipt.vaccineName.toLocaleUpperCase();
|
||||
let vaccineNameProper = vaccineName.charAt(0) + vaccineName.substr(1).toLowerCase();
|
||||
|
||||
if (vaccineName.includes('PFIZER'))
|
||||
@ -63,42 +105,58 @@ export class Payload {
|
||||
if (vaccineName.includes('ASTRAZENECA') || vaccineName.includes('COVISHIELD'))
|
||||
vaccineNameProper = 'AstraZeneca (Vaxzevria)'
|
||||
|
||||
let doseVaccine = "#" + String(body.receipts[numDose].numDoses) + ": " + vaccineNameProper;
|
||||
|
||||
if (name == undefined) {
|
||||
throw new Error('nameMissing');
|
||||
}
|
||||
if (dateOfBirth == undefined) {
|
||||
throw new Error('dobMissing');
|
||||
let doseVaccine = "#" + String(receipt.numDoses) + ": " + vaccineNameProper;
|
||||
let fullyVaccinated = false;
|
||||
|
||||
if (receipt.numDoses > 1 ||
|
||||
vaccineName.toLowerCase().includes('janssen') ||
|
||||
vaccineName.toLowerCase().includes('johnson') ||
|
||||
vaccineName.toLowerCase().includes('j&j')) {
|
||||
fullyVaccinated = true;
|
||||
}
|
||||
|
||||
const generic: PassDictionary = {
|
||||
headerFields: [
|
||||
],
|
||||
primaryFields: [
|
||||
if (generic.primaryFields.length == 0) {
|
||||
generic.primaryFields.push(
|
||||
{
|
||||
key: "vaccine",
|
||||
label: "Vaccine",
|
||||
value: doseVaccine,
|
||||
key: "vaccine",
|
||||
label: "Vaccine",
|
||||
value: doseVaccine
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
],
|
||||
secondaryFields: [
|
||||
{
|
||||
let fieldToPush = generic.secondaryFields;
|
||||
if (fieldToPush.length > 0) {
|
||||
fieldToPush = generic.backFields;
|
||||
generic.headerFields.push({
|
||||
key: "extra",
|
||||
label: "More",
|
||||
value: "(i)",
|
||||
"textAlignment" : "PKTextAlignmentCenter"
|
||||
});
|
||||
generic.backFields.push({
|
||||
key: "vaccine" + numDoses,
|
||||
label: `Vaccine (Dose ${numDoses})`,
|
||||
value: receipt.vaccineName
|
||||
})
|
||||
}
|
||||
|
||||
fieldToPush.push(
|
||||
{
|
||||
key: "issuer",
|
||||
label: "Authorized Organization",
|
||||
value: body.receipts[numDose].organization
|
||||
},
|
||||
|
||||
value: receipt.organization
|
||||
},
|
||||
{
|
||||
key: "dov",
|
||||
label: "Date",
|
||||
value: body.receipts[numDose].vaccinationDate,
|
||||
// textAlignment: TextAlignment.right
|
||||
value: receipt.vaccinationDate,
|
||||
}
|
||||
],
|
||||
auxiliaryFields: [
|
||||
{
|
||||
);
|
||||
|
||||
if (generic.auxiliaryFields.length == 0) {
|
||||
generic.auxiliaryFields.push(
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
value: name
|
||||
@ -107,33 +165,7 @@ export class Payload {
|
||||
key: "dob",
|
||||
label: "Date of Birth",
|
||||
value: dateOfBirth
|
||||
}
|
||||
],
|
||||
backFields: [
|
||||
|
||||
//TODO: add url link back to grassroots site
|
||||
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Set Values
|
||||
this.receipt = body.receipts[numDose];
|
||||
this.rawData = body.rawData;
|
||||
|
||||
if (body.receipts[numDose].numDoses > 1 || body.receipts[numDose].vaccineName.toLowerCase().includes('janssen') || body.receipts[numDose].vaccineName.toLowerCase().includes('johnson') || body.receipts[numDose].vaccineName.toLowerCase().includes('j&j')) {
|
||||
this.backgroundColor = COLORS.GREEN;
|
||||
} else {
|
||||
this.backgroundColor = COLORS.YELLOW;
|
||||
}
|
||||
|
||||
this.labelColor = COLORS.WHITE
|
||||
this.foregroundColor = COLORS.WHITE
|
||||
this.img1x = Constants.img1xWhite
|
||||
this.img2x = Constants.img2xWhite
|
||||
this.generic = generic;
|
||||
|
||||
return fullyVaccinated;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
125
src/photo.ts
125
src/photo.ts
@ -4,25 +4,11 @@ import {v4 as uuid4} from 'uuid';
|
||||
import {BrowserQRCodeSvgWriter} from "@zxing/browser";
|
||||
import { toPng, toJpeg, toBlob, toPixelData, toSvg } from 'html-to-image';
|
||||
import * as Sentry from '@sentry/react';
|
||||
|
||||
enum QrFormat {
|
||||
PKBarcodeFormatQR = 'PKBarcodeFormatQR',
|
||||
PKBarcodeFormatPDF417 = 'PKBarcodeFormatPDF417'
|
||||
}
|
||||
|
||||
enum Encoding {
|
||||
utf8 = "utf-8",
|
||||
iso88591 = "iso-8859-1"
|
||||
}
|
||||
|
||||
interface QrCode {
|
||||
message: string;
|
||||
format: QrFormat;
|
||||
messageEncoding: Encoding;
|
||||
// altText: string;
|
||||
}
|
||||
import {QrCode,Encoding,PackageResult,QrFormat,PassPhotoCommon} from './passphoto-common';
|
||||
import { EncodeHintType } from "@zxing/library";
|
||||
|
||||
export class Photo {
|
||||
|
||||
logoText: string = Constants.NAME;
|
||||
organizationName: string = Constants.NAME;
|
||||
description: string = Constants.NAME;
|
||||
@ -33,99 +19,62 @@ export class Photo {
|
||||
barcodes: Array<QrCode>;
|
||||
barcode: QrCode;
|
||||
|
||||
|
||||
|
||||
static async generatePass(payloadBody: PayloadBody, numDose: number): Promise<Blob> {
|
||||
|
||||
// Create Payload
|
||||
try {
|
||||
const payload: Payload = new Payload(payloadBody, numDose);
|
||||
console.log('generatePass');
|
||||
const results = await PassPhotoCommon.preparePayload(payloadBody, numDose);
|
||||
|
||||
const payload = results.payload;
|
||||
const qrCode = results.qrCode;
|
||||
|
||||
payload.serialNumber = uuid4();
|
||||
|
||||
// register record
|
||||
|
||||
const clonedReceipt = Object.assign({}, payload.receipt);
|
||||
delete clonedReceipt.name;
|
||||
delete clonedReceipt.dateOfBirth;
|
||||
clonedReceipt["serialNumber"] = payload.serialNumber;
|
||||
clonedReceipt["type"] = 'photo';
|
||||
|
||||
let requestOptions = {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(clonedReceipt) // body data type must match "Content-Type" header
|
||||
let receipt;
|
||||
if (results.payload.rawData.length == 0) {
|
||||
receipt = results.payload.receipts[numDose];
|
||||
} else {
|
||||
receipt = results.payload.receipts[numDose];
|
||||
}
|
||||
|
||||
console.log('registering ' + JSON.stringify(clonedReceipt, null, 2));
|
||||
const configResponse = await fetch('/api/config')
|
||||
const verifierHost = (await configResponse.json()).verifierHost
|
||||
|
||||
// const verifierHost = 'https://verifier.vaccine-ontario.ca';
|
||||
|
||||
const response = await fetch('https://us-central1-grassroot-verifier.cloudfunctions.net/register', requestOptions);
|
||||
const responseJson = await response.json();
|
||||
|
||||
console.log(JSON.stringify(responseJson,null,2));
|
||||
|
||||
if (responseJson["result"] != 'OK')
|
||||
return Promise.reject();
|
||||
|
||||
const encodedUri = `serialNumber=${encodeURIComponent(payload.serialNumber)}&vaccineName=${encodeURIComponent(payload.receipt.vaccineName)}&vaccinationDate=${encodeURIComponent(payload.receipt.vaccinationDate)}&organization=${encodeURIComponent(payload.receipt.organization)}&dose=${encodeURIComponent(payload.receipt.numDoses)}`;
|
||||
const qrCodeUrl = `${verifierHost}/verify?${encodedUri}`;
|
||||
|
||||
// Create QR Code Object
|
||||
const qrCode: QrCode = {
|
||||
message: qrCodeUrl,
|
||||
format: QrFormat.PKBarcodeFormatQR,
|
||||
messageEncoding: Encoding.iso88591,
|
||||
// altText : payload.rawData
|
||||
|
||||
}
|
||||
|
||||
// Create photo
|
||||
// const photo: Photo = new Photo(payload, qrCode);
|
||||
|
||||
// const body = domTree.getElementById('main');
|
||||
const body = document.getElementById('pass-image');
|
||||
body.hidden = false;
|
||||
body.style.backgroundColor = payload.backgroundColor
|
||||
|
||||
const name = payload.receipt.name;
|
||||
const dateOfBirth = payload.receipt.dateOfBirth;
|
||||
const vaccineName = payload.receipt.vaccineName;
|
||||
let vaccineNameProper = vaccineName.charAt(0) + vaccineName.substr(1).toLowerCase();
|
||||
|
||||
if (vaccineName.includes('PFIZER'))
|
||||
vaccineNameProper = 'Pfizer (Comirnaty)'
|
||||
|
||||
if (vaccineName.includes('MODERNA'))
|
||||
vaccineNameProper = 'Moderna (SpikeVax)'
|
||||
|
||||
if (vaccineName.includes('ASTRAZENECA') || vaccineName.includes('COVISHIELD'))
|
||||
vaccineNameProper = 'AstraZeneca (Vaxzevria)'
|
||||
|
||||
let doseVaccine = "#" + String(payload.receipt.numDoses) + ": " + vaccineNameProper;
|
||||
const vaccineName = receipt.vaccineName;
|
||||
let doseVaccine = "#" + String(receipt.numDoses) + ": " + vaccineName;
|
||||
|
||||
document.getElementById('vaccineName').innerText = doseVaccine;
|
||||
document.getElementById('vaccinationDate').innerText = payload.receipt.vaccinationDate;
|
||||
document.getElementById('organization').innerText = payload.receipt.organization;
|
||||
document.getElementById('name').innerText = payload.receipt.name;
|
||||
document.getElementById('dob').innerText = payload.receipt.dateOfBirth;
|
||||
|
||||
document.getElementById('vaccinationDate').innerText = receipt.vaccinationDate;
|
||||
document.getElementById('organization').innerText = receipt.organization;
|
||||
|
||||
document.getElementById('name').innerText = receipt.name;
|
||||
document.getElementById('dob').innerText = receipt.dateOfBirth;
|
||||
|
||||
if ((results.payload.rawData.length != 0) && (numDose > 1)) {
|
||||
for (let i = 1; i < numDose; i++) {
|
||||
|
||||
console.log(i);
|
||||
|
||||
receipt = results.payload.receipts[i];
|
||||
|
||||
document.getElementById('extraRow' + i ).hidden = false;
|
||||
document.getElementById('vaccinationDate' + i).innerText = receipt.vaccinationDate;
|
||||
document.getElementById('organization' + i).innerText = receipt.organization;
|
||||
}
|
||||
}
|
||||
|
||||
const codeWriter = new BrowserQRCodeSvgWriter();
|
||||
const svg = codeWriter.write(qrCode.message,200,200);
|
||||
const hints : Map<EncodeHintType,any> = new Map().set(EncodeHintType.ERROR_CORRECTION,'L');
|
||||
const svg = codeWriter.write(qrCode.message,200,200, hints);
|
||||
svg.setAttribute('style','background-color: white');
|
||||
document.getElementById('qrcode').appendChild(svg);
|
||||
|
||||
const blobPromise = toBlob(body);
|
||||
return blobPromise;
|
||||
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return Promise.reject();
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
126
src/process.ts
126
src/process.ts
@ -1,10 +1,14 @@
|
||||
import {PayloadBody, Receipt, HashTable} from "./payload";
|
||||
import * as PdfJS from 'pdfjs-dist'
|
||||
import jsQR, {QRCode} from "jsqr";
|
||||
import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6
|
||||
import {COLORS} from "./colors";
|
||||
import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6
|
||||
import * as Sentry from '@sentry/react';
|
||||
import * as Decode from './decode';
|
||||
import {getScannedJWS, verifyJWS, decodeJWS} from "./shc";
|
||||
import { PNG } from 'pngjs/browser';
|
||||
|
||||
import { TextItem } from "pdfjs-dist/types/display/api";
|
||||
import { PDFPageProxy, TextContent, TextItem } from "pdfjs-dist/types/display/api";
|
||||
|
||||
// import {PNG} from 'pngjs'
|
||||
// import {decodeData} from "./decode";
|
||||
@ -12,33 +16,67 @@ import { TextItem } from "pdfjs-dist/types/display/api";
|
||||
|
||||
PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PdfJS.version}/pdf.worker.js`
|
||||
|
||||
export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise<PayloadBody> {
|
||||
export async function getPayloadBodyFromFile(file: File): Promise<PayloadBody> {
|
||||
// Read file
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
let receipts: HashTable<Receipt>;
|
||||
let rawData = ''; // unused at the moment, the original use was to store the QR code from issuer
|
||||
|
||||
switch (file.type) {
|
||||
case 'application/pdf':
|
||||
receipts = await loadPDF(fileBuffer)
|
||||
const receiptType = await detectReceiptType(fileBuffer);
|
||||
console.log(`receiptType = ${receiptType}`);
|
||||
if (receiptType == 'ON') {
|
||||
receipts = await loadPDF(fileBuffer) // receipt type is needed to decide if digital signature checking is needed
|
||||
} else {
|
||||
const shcData = await processSHC(fileBuffer);
|
||||
receipts = shcData.receipts;
|
||||
rawData = shcData.rawData;
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw Error('invalidFileType')
|
||||
}
|
||||
|
||||
const rawData = ''; // unused at the moment, the original use was to store the QR code from issuer
|
||||
|
||||
return {
|
||||
receipts: receipts,
|
||||
rawData: rawData
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise<HashTable<Receipt>> {
|
||||
async function detectReceiptType(fileBuffer : ArrayBuffer): Promise<string> {
|
||||
|
||||
// Ontario has 'COVID-19 vaccination receipt'
|
||||
// BC has BC Vaccine Card
|
||||
|
||||
console.log('detectPDFTypeAndProcess');
|
||||
|
||||
const typedArray = new Uint8Array(fileBuffer);
|
||||
let loadingTask = PdfJS.getDocument(typedArray);
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
const pdfPage = await pdfDocument.getPage(1); //first page
|
||||
const content = await pdfPage.getTextContent();
|
||||
const numItems = content.items.length;
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
let item = content.items[i] as TextItem;
|
||||
const value = item.str;
|
||||
console.log(value);
|
||||
if (value.includes('COVID-19 vaccination receipt')) {
|
||||
console.log('detected on');
|
||||
return Promise.resolve('ON');
|
||||
}
|
||||
}
|
||||
return Promise.resolve('SHC');
|
||||
|
||||
}
|
||||
|
||||
async function loadPDF(fileBuffer : ArrayBuffer): Promise<HashTable<Receipt>> {
|
||||
|
||||
try {
|
||||
|
||||
const certs = getCertificatesInfoFromPDF(signedPdfBuffer);
|
||||
const certs = getCertificatesInfoFromPDF(fileBuffer);
|
||||
|
||||
const result = certs[0];
|
||||
const refcert = '-----BEGIN CERTIFICATE-----\r\n'+
|
||||
@ -95,7 +133,7 @@ async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise<HashTable<Receipt
|
||||
|
||||
if (( issuedpemCertificate )) {
|
||||
//console.log('getting receipt details inside PDF');
|
||||
const receipt = await getPdfDetails(signedPdfBuffer);
|
||||
const receipt = await getPdfDetails(fileBuffer);
|
||||
// console.log(JSON.stringify(receipt, null, 2));
|
||||
return Promise.resolve(receipt);
|
||||
|
||||
@ -110,13 +148,16 @@ async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise<HashTable<Receipt
|
||||
console.error('invalid certificate');
|
||||
return Promise.reject(`invalid certificate + ${JSON.stringify(result)}`);
|
||||
}
|
||||
|
||||
|
||||
} catch (e) {
|
||||
|
||||
console.error(e);
|
||||
Sentry.captureException(e);
|
||||
|
||||
if (e.message.includes('Failed to locate ByteRange')) {
|
||||
e.message = 'Sorry. Selected PDF file is not digitally signed. Please download official copy from Step 1 and retry. Thanks.'
|
||||
} else {
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
return Promise.reject(e);
|
||||
}
|
||||
@ -159,6 +200,7 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt
|
||||
numDoses = Number(value.split(' ')[3]);
|
||||
}
|
||||
receiptObj[numDoses] = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, numDoses, organization);
|
||||
console.log(receiptObj[numDoses]);
|
||||
}
|
||||
|
||||
return Promise.resolve(receiptObj);
|
||||
@ -167,3 +209,65 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function getImageDataFromPdf(pdfPage: PDFPageProxy): Promise<ImageData> {
|
||||
|
||||
const pdfScale = 2;
|
||||
|
||||
const canvas = <HTMLCanvasElement>document.getElementById('canvas');
|
||||
const canvasContext = canvas.getContext('2d');
|
||||
const viewport = pdfPage.getViewport({scale: pdfScale})
|
||||
|
||||
// Set correct canvas width / height
|
||||
canvas.width = viewport.width
|
||||
canvas.height = viewport.height
|
||||
|
||||
// render PDF
|
||||
const renderTask = pdfPage.render({
|
||||
canvasContext: canvasContext,
|
||||
viewport,
|
||||
})
|
||||
|
||||
await renderTask.promise;
|
||||
|
||||
// Return PDF Image Data
|
||||
return canvasContext.getImageData(0, 0, canvas.width, canvas.height)
|
||||
|
||||
}
|
||||
|
||||
async function processSHC(fileBuffer : ArrayBuffer) : Promise<any> {
|
||||
|
||||
console.log('processSHC');
|
||||
|
||||
try {
|
||||
const typedArray = new Uint8Array(fileBuffer);
|
||||
let loadingTask = PdfJS.getDocument(typedArray);
|
||||
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
// Load all dose numbers
|
||||
const pdfPage = await pdfDocument.getPage(1);
|
||||
const imageData = await getImageDataFromPdf(pdfPage);
|
||||
const code : QRCode = await Decode.getQRFromImage(imageData);
|
||||
let rawData = code.data;
|
||||
const jws = getScannedJWS(rawData);
|
||||
|
||||
let decoded = await decodeJWS(jws);
|
||||
|
||||
// console.log(decoded);
|
||||
|
||||
const verified = verifyJWS(jws, decoded.iss);
|
||||
|
||||
if (verified) {
|
||||
let receipts = Decode.decodedStringToReceipt(decoded);
|
||||
console.log(receipts);
|
||||
return Promise.resolve({receipts: receipts, rawData: rawData});
|
||||
|
||||
} else {
|
||||
return Promise.reject(`Issuer ${decoded.iss} cannot be verified.`);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
Promise.reject(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,7 +8,9 @@ export const initSentry = () => {
|
||||
integrations: [
|
||||
new Integrations.BrowserTracing(),
|
||||
],
|
||||
attachStacktrace: true
|
||||
attachStacktrace: true,
|
||||
tracesSampleRate: 0.5
|
||||
|
||||
});
|
||||
console.log('sentry initialized');
|
||||
|
||||
|
78
src/shc.js
Normal file
78
src/shc.js
Normal file
@ -0,0 +1,78 @@
|
||||
const jose = require("node-jose");
|
||||
const jsQR = require("jsqr");
|
||||
const zlib = require("zlib");
|
||||
const { issuers } = require("./issuers");
|
||||
|
||||
function getQRFromImage(imageData) {
|
||||
return jsQR(
|
||||
new Uint8ClampedArray(imageData.data.buffer),
|
||||
imageData.width,
|
||||
imageData.height
|
||||
);
|
||||
}
|
||||
|
||||
function getScannedJWS(shcString) {
|
||||
try {
|
||||
return shcString
|
||||
.match(/^shc:\/(.+)$/)[1]
|
||||
.match(/(..?)/g)
|
||||
.map((num) => String.fromCharCode(parseInt(num, 10) + 45))
|
||||
.join("");
|
||||
} catch (e) {
|
||||
error = new Error("parsing shc string failed");
|
||||
error.cause = e;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function verifyJWS(jws, iss) {
|
||||
const issuer = issuers.find(el => el.iss === iss);
|
||||
if (!issuer) {
|
||||
error = new Error("Unknown issuer " + iss);
|
||||
error.customMessage = true;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return jose.JWK.asKeyStore({ keys: issuer.keys }).then(function (keyStore) {
|
||||
const { verify } = jose.JWS.createVerify(keyStore);
|
||||
console.log("jws", jws);
|
||||
return verify(jws);
|
||||
});
|
||||
}
|
||||
|
||||
function decodeJWS(jws) {
|
||||
try {
|
||||
const payload = jws.split(".")[1];
|
||||
return decodeJWSPayload(Buffer.from(payload, "base64"));
|
||||
} catch (e) {
|
||||
error = new Error("decoding payload failed");
|
||||
error.cause = e;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function decodeJWSPayload(decodedPayload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
zlib.inflateRaw(decodedPayload, function (err, decompressedResult) {
|
||||
if (typeof err === "object" && err) {
|
||||
console.log("Unable to decompress");
|
||||
reject(err);
|
||||
} else {
|
||||
try {
|
||||
console.log(decompressedResult);
|
||||
scannedResult = decompressedResult.toString("utf8");
|
||||
resolve(JSON.parse(scannedResult));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getQRFromImage,
|
||||
getScannedJWS,
|
||||
verifyJWS,
|
||||
decodeJWS,
|
||||
decodeJWSPayload,
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user