diff --git a/components/Form.tsx b/components/Form.tsx
index 2e622cf..d28cd72 100644
--- a/components/Form.tsx
+++ b/components/Form.tsx
@@ -96,7 +96,7 @@ function Form(): JSX.Element {
// Add event listener to listen for file change events
useEffect(() => {
if (inputFile && inputFile.current) {
- inputFile.current.addEventListener('input', () => {
+ inputFile.current.addEventListener('change', () => {
let selectedFile = inputFile.current.files[0];
if (selectedFile !== undefined) {
setFileLoading(true);
@@ -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 {
diff --git a/components/Page.tsx b/components/Page.tsx
index 36d2d0b..79fa7c1 100644
--- a/components/Page.tsx
+++ b/components/Page.tsx
@@ -36,7 +36,7 @@ function Page(props: PageProps): JSX.Element {
{t('common:gitHub')}
{t('common:returnToMainSite')}
-
Last updated: 2021-09-29 (v1.9.12)
+ Last updated: 2021-10-05 (v1.9.15)
@@ -48,14 +48,12 @@ function Page(props: PageProps): JSX.Element {
|
- Vaccination Receipt |
+ Vaccination Receipt |
-
-
VACCINE
@@ -64,12 +62,20 @@ function Page(props: PageProps): JSX.Element {
AUTHORIZED ORGANIZATION |
- DATE |
+ VACC. DATE |
|
|
+
+
NAME |
@@ -79,16 +85,24 @@ function Page(props: PageProps): JSX.Element {
|
|
+
+
+ |
+ QR CODE EXPIRY |
+
+
+ |
+ 2021-10-22 |
+
-
-
+
)
}
diff --git a/package-lock.json b/package-lock.json
index 10331e3..687294b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "grassroots_covidpass",
- "version": "1.8.0",
+ "version": "1.9.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "grassroots_covidpass",
- "version": "1.8.0",
+ "version": "1.9.7",
"license": "MIT",
"dependencies": {
"@headlessui/react": "^1.3.0",
@@ -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",
@@ -48,6 +49,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",
@@ -498,6 +500,15 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "node_modules/@types/pngjs": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz",
+ "integrity": "sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/prop-types": {
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
@@ -911,6 +922,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 +1751,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 +2733,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 +3090,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",
@@ -5656,6 +5710,15 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "@types/pngjs": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz",
+ "integrity": "sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/prop-types": {
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
@@ -6000,6 +6063,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 +6762,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 +7485,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 +7751,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",
diff --git a/package.json b/package.json
index 99ab19b..a334f8c 100644
--- a/package.json
+++ b/package.json
@@ -40,8 +40,9 @@
"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",
+ "pdfjs-dist": "^2.6.347",
"pngjs": "^6.0.0",
"qrcode": "^1.4.4",
"react": "^17.0.2",
@@ -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",
diff --git a/pages/index.tsx b/pages/index.tsx
index 03ccd21..5a25978 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -74,12 +74,12 @@ function Index(): JSX.Element {
{t('common:subtitle')}
{t('common:subtitle2')}
{displayPassCount}
- Sept 29 afternoon update:
+ Oct 3 evening update:
- - You can now select which page to import for multi-page receipts
- - System reminders (e.g. unsupported browsers) are now on the top to improve ease of use
+ - Added expiration date to Apple Wallet pass so it aligns with the province's schedule.
+ - On Oct 22, we will update this tool as well so you can import the official QR code into your mobile wallet too.
{t('common:continueSpirit')}
diff --git a/public/locales/en/common.yml b/public/locales/en/common.yml
index 3551e5e..59db56d 100644
--- a/public/locales/en/common.yml
+++ b/public/locales/en/common.yml
@@ -1,6 +1,6 @@
title: Vaccination Receipt to Wallet
-subtitle: This utility (created by volunteers) converts your vaccination receipt from Ontario Ministry of Health to an Apple Wallet pass for easy access in the interim.
-subtitle2: Once Ontario's official QR code is available on Oct 22, you will be able to update your Apple Wallet pass by visiting this site again.
+subtitle: This utility (created by volunteers) converts your vaccination receipt from Ontario Ministry of Health to an Apple Wallet pass for easy access in the interim. Android users can create a photo pass while we await Google Pay COVID Card API Access from Google.
+subtitle2: Once Ontario's official QR code is available on Oct 22, you will be able to update your Apple Wallet pass or Android photo pass by visiting this site again.
update1Date: Sep 23 Updates
update1: Thanks so much for all the encouragements and suggestions to make this better. We plan to keep enhancing this to help more Canadians. Stay tuned!
continueSpirit: Continuing the spirit of ❤️ @VaxHuntersCanada ❤️.
diff --git a/src/decode.ts b/src/decode.ts
index ae20528..563c736 100644
--- a/src/decode.ts
+++ b/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 {
- 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 = {};
+
+ 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 = 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;
}
\ No newline at end of file
diff --git a/src/issuers.js b/src/issuers.js
new file mode 100644
index 0000000..e87321b
--- /dev/null
+++ b/src/issuers.js
@@ -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,
+};
diff --git a/src/pass.ts b/src/pass.ts
index bbe6048..fec58ab 100644
--- a/src/pass.ts
+++ b/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;
@@ -46,6 +30,7 @@ export class PassData {
barcodes: Array;
barcode: QrCode;
generic: PassDictionary;
+ expirationDate: string;
// Generates a sha1 hash from a given buffer
private static getBufferHash(buffer: Buffer | string): string {
@@ -85,67 +70,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 +127,7 @@ export class PassData {
return createZip(zip);
} catch (e) {
- Sentry.captureException(e);
- return Promise.reject();
+ return Promise.reject(e);
}
}
@@ -211,5 +140,6 @@ export class PassData {
this.barcode = qrCode;
this.generic = payload.generic;
this.sharingProhibited = true;
+ this.expirationDate = payload.expirationDate;
}
}
diff --git a/src/passphoto-common.ts b/src/passphoto-common.ts
new file mode 100644
index 0000000..771369a
--- /dev/null
+++ b/src/passphoto-common.ts
@@ -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 {
+
+ 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}
+
+ }
+}
\ No newline at end of file
diff --git a/src/payload.ts b/src/payload.ts
index 8e92b85..735ec7c 100644
--- a/src/payload.ts
+++ b/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;
rawData: string;
backgroundColor: string;
labelColor: string;
@@ -45,61 +46,125 @@ export class Payload {
img2x: Buffer;
serialNumber: string;
generic: PassDictionary;
+ expirationDate: string;
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;
+ if (body.rawData.length == 0) { // Ontario special handling
+ this.expirationDate = '2021-10-22T23:59:59-04:00';
+ generic.auxiliaryFields.push({
+ key: "expiry",
+ label: "QR code expiry",
+ value: '2021-10-22'
+ })
+ }
+ }
+}
+
+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'))
vaccineNameProper = 'Pfizer (Comirnaty)'
if (vaccineName.includes('MODERNA'))
- vaccineNameProper = 'Moderna (SpikeVax)'
- // vaccineNameProper = 'Pfizer (Comirnaty)'
+ vaccineNameProper = 'Moderna (SpikeVax)'
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
+ label: "Vacc. Date",
+ value: receipt.vaccinationDate,
}
- ],
- auxiliaryFields: [
- {
+ );
+
+ if (generic.auxiliaryFields.length == 0) {
+ generic.auxiliaryFields.push(
+ {
key: "name",
label: "Name",
value: name
@@ -108,33 +173,8 @@ 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;
}
-
-
-
-}
\ No newline at end of file
diff --git a/src/photo.ts b/src/photo.ts
index 4ea9bd2..0ae97d8 100644
--- a/src/photo.ts
+++ b/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,73 @@ export class Photo {
barcodes: Array;
barcode: QrCode;
-
-
static async generatePass(payloadBody: PayloadBody, numDose: number): Promise {
// 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;
+ const vaccineName = receipt.vaccineName.toLocaleUpperCase();
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;
+ vaccineNameProper = 'AstraZeneca (Vaxzevria)'
+
+ let doseVaccine = "#" + String(receipt.numDoses) + ": " + vaccineNameProper;
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 = 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);
}
}
diff --git a/src/process.ts b/src/process.ts
index 48e2fcc..13d9c36 100644
--- a/src/process.ts
+++ b/src/process.ts
@@ -1,10 +1,14 @@
import {PayloadBody, Receipt, HashTable} from "./payload";
-import * as PdfJS from 'pdfjs-dist'
+import * as PdfJS from 'pdfjs-dist/legacy/build/pdf'
+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/src/display/api';
// import {PNG} from 'pngjs'
// import {decodeData} from "./decode";
@@ -12,33 +16,72 @@ 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 {
+export async function getPayloadBodyFromFile(file: File): Promise {
// Read file
const fileBuffer = await file.arrayBuffer();
-
let receipts: HashTable;
+ 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> {
+async function detectReceiptType(fileBuffer : ArrayBuffer): Promise {
+
+ // 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;
+ if (numItems == 0) { // QC has no text items
+ console.log('detected QC');
+ return Promise.resolve('SHC');
+ } else {
+ for (let i = 0; i < numItems; i++) {
+ let item = content.items[i] as TextItem;
+ const value = item.str;
+ // console.log(value);
+ if (value.includes('BC Vaccine Card')) {
+ console.log('detected BC');
+ return Promise.resolve('SHC');
+ }
+ }
+ }
+ return Promise.resolve('ON');
+
+}
+
+async function loadPDF(fileBuffer : ArrayBuffer): Promise> {
try {
- const certs = getCertificatesInfoFromPDF(signedPdfBuffer);
+ const certs = getCertificatesInfoFromPDF(fileBuffer);
const result = certs[0];
const refcert = '-----BEGIN CERTIFICATE-----\r\n'+
@@ -95,7 +138,7 @@ async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise> {
@@ -153,7 +196,6 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise {
+
+ const pdfScale = 2;
+
+ const canvas = 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 {
+
+ 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);
+ }
}
diff --git a/src/sentry.ts b/src/sentry.ts
index d3d2785..3f8e182 100644
--- a/src/sentry.ts
+++ b/src/sentry.ts
@@ -3,12 +3,14 @@ import { Integrations } from '@sentry/tracing';
export const initSentry = () => {
SentryModule.init({
- release: 'grassroots_covidpass@1.9.12', // App version. Needs to be manually updated as we go unless we make the build smarter
+ release: 'grassroots_covidpass@1.9.15', // App version. Needs to be manually updated as we go unless we make the build smarter
dsn: 'https://7120dcf8548c4c5cb148cdde2ed6a778@o1015766.ingest.sentry.io/5981424',
integrations: [
new Integrations.BrowserTracing(),
],
- attachStacktrace: true
+ attachStacktrace: true,
+ tracesSampleRate: 0.5
+
});
console.log('sentry initialized');
diff --git a/src/shc.js b/src/shc.js
new file mode 100644
index 0000000..5e60e16
--- /dev/null
+++ b/src/shc.js
@@ -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,
+};
diff --git a/yarn.lock b/yarn.lock
index 55e7e67..39ae521 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -286,6 +286,13 @@
"resolved" "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz"
"version" "4.0.0"
+"@types/pngjs@^6.0.1":
+ "integrity" "sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg=="
+ "resolved" "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz"
+ "version" "6.0.1"
+ dependencies:
+ "@types/node" "*"
+
"@types/prop-types@*":
"integrity" "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
"resolved" "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz"
@@ -625,6 +632,11 @@
"resolved" "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
"version" "1.5.1"
+"base64url@^3.0.1":
+ "integrity" "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
+ "resolved" "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz"
+ "version" "3.0.1"
+
"big.js@^5.2.2":
"integrity" "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="
"resolved" "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz"
@@ -789,7 +801,7 @@
"ieee754" "^1.1.4"
"isarray" "^1.0.0"
-"buffer@^5.4.3", "buffer@5.6.0":
+"buffer@^5.4.3", "buffer@^5.5.0", "buffer@5.6.0":
"integrity" "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="
"resolved" "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz"
"version" "5.6.0"
@@ -1305,6 +1317,11 @@
"resolved" "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz"
"version" "1.1.0"
+"es6-promise@^4.2.8":
+ "integrity" "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
+ "resolved" "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz"
+ "version" "4.2.8"
+
"escalade@^3.1.1":
"integrity" "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
"resolved" "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz"
@@ -2018,11 +2035,16 @@
"resolved" "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz"
"version" "4.5.2"
-"lodash@^4.17.13", "lodash@^4.17.21":
+"lodash@^4.17.13", "lodash@^4.17.15", "lodash@^4.17.21":
"integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
"version" "4.17.21"
+"long@^4.0.0":
+ "integrity" "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+ "resolved" "https://registry.npmjs.org/long/-/long-4.0.0.tgz"
+ "version" "4.0.0"
+
"loose-envify@^1.1.0", "loose-envify@^1.4.0":
"integrity" "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="
"resolved" "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
@@ -2232,6 +2254,21 @@
dependencies:
"he" "1.2.0"
+"node-jose@^2.0.0":
+ "integrity" "sha512-j8zoFze1gijl8+DK/dSXXqX7+o2lMYv1XS+ptnXgGV/eloQaqq1YjNtieepbKs9jBS4WTnMOqyKSaQuunJzx0A=="
+ "resolved" "https://registry.npmjs.org/node-jose/-/node-jose-2.0.0.tgz"
+ "version" "2.0.0"
+ 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-libs-browser@^2.2.1":
"integrity" "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q=="
"resolved" "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz"
@@ -2380,7 +2417,7 @@
"resolved" "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
"version" "2.2.0"
-"pako@~1.0.5":
+"pako@^1.0.11", "pako@~1.0.5":
"integrity" "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
"resolved" "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz"
"version" "1.0.11"
@@ -3380,6 +3417,11 @@
dependencies:
"base64-arraybuffer" "^1.0.1"
+"uuid@^3.3.3":
+ "integrity" "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+ "resolved" "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"
+ "version" "3.4.0"
+
"uuid@^8.3.2":
"integrity" "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
"resolved" "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"