commit
8eac24880d
|
@ -117,15 +117,17 @@ function Form(): JSX.Element {
|
||||||
|
|
||||||
async function getPayload(file){
|
async function getPayload(file){
|
||||||
try {
|
try {
|
||||||
const payload = await getPayloadBodyFromFile(file, COLORS.GREEN);
|
const payload = await getPayloadBodyFromFile(file);
|
||||||
setPayloadBody(payload);
|
setPayloadBody(payload);
|
||||||
setFileLoading(false);
|
setFileLoading(false);
|
||||||
setFile(file);
|
setFile(file);
|
||||||
|
|
||||||
if (Object.keys(payload.receipts).length === 1) {
|
if (payload.rawData.length == 0) {
|
||||||
setSelectedDose(parseInt(Object.keys(payload.receipts)[0]));
|
if (Object.keys(payload.receipts).length === 1) {
|
||||||
}else{
|
setSelectedDose(parseInt(Object.keys(payload.receipts)[0]));
|
||||||
setShowDoseOption(true);
|
} else {
|
||||||
|
setShowDoseOption(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setFile(file);
|
setFile(file);
|
||||||
|
@ -253,21 +255,30 @@ function Form(): JSX.Element {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (payloadBody) {
|
if (payloadBody) {
|
||||||
const passName = payloadBody.receipts[selectedDose].name.replace(' ', '-');
|
|
||||||
const vaxName = payloadBody.receipts[selectedDose].vaccineName.replace(' ', '-');
|
let selectedReceipt;
|
||||||
const passDose = payloadBody.receipts[selectedDose].numDoses;
|
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`;
|
const covidPassFilename = `grassroots-receipt-${passName}-${vaxName}-${passDose}.pkpass`;
|
||||||
|
|
||||||
//console.log('> increment count');
|
console.log('> increment count');
|
||||||
await incrementCount();
|
await incrementCount();
|
||||||
|
|
||||||
// console.log('> generatePass');
|
console.log('> generatePass');
|
||||||
const pass = await PassData.generatePass(payloadBody, selectedDose);
|
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"});
|
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);
|
saveAs(passBlob, covidPassFilename);
|
||||||
setSaveLoading(false);
|
setSaveLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -308,13 +319,24 @@ function Form(): JSX.Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const passName = payloadBody.receipts[selectedDose].name.replace(' ', '-');
|
|
||||||
const vaxName = payloadBody.receipts[selectedDose].vaccineName.replace(' ', '-');
|
let selectedReceipt;
|
||||||
const passDose = payloadBody.receipts[selectedDose].numDoses;
|
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`;
|
const covidPassFilename = `grassroots-receipt-${passName}-${vaxName}-${passDose}.png`;
|
||||||
|
|
||||||
await incrementCount();
|
await incrementCount();
|
||||||
let photoBlob = await Photo.generatePass(payloadBody, selectedDose);
|
|
||||||
|
let photoBlob = await Photo.generatePass(payloadBody, passDose);
|
||||||
saveAs(photoBlob, covidPassFilename);
|
saveAs(photoBlob, covidPassFilename);
|
||||||
|
|
||||||
// need to clean up
|
// need to clean up
|
||||||
|
@ -420,7 +442,7 @@ function Form(): JSX.Element {
|
||||||
|
|
||||||
<input type='file'
|
<input type='file'
|
||||||
id='file'
|
id='file'
|
||||||
accept="application/pdf"
|
accept="application/pdf,image/png"
|
||||||
ref={inputFile}
|
ref={inputFile}
|
||||||
style={{display: 'none'}}
|
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://github.com/billylo1/covidpass" className="underline">{t('common:gitHub')}</a>
|
||||||
<a href="https://vaccine-ontario.ca" className="underline">{t('common:returnToMainSite')}</a>
|
<a href="https://vaccine-ontario.ca" className="underline">{t('common:returnToMainSite')}</a>
|
||||||
</nav>
|
</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>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,6 +70,14 @@ function Page(props: PageProps): JSX.Element {
|
||||||
<td id='organization' style={{width: 220}}></td>
|
<td id='organization' style={{width: 220}}></td>
|
||||||
<td id='vaccinationDate' style={{width:120}}></td>
|
<td id='vaccinationDate' style={{width:120}}></td>
|
||||||
</tr>
|
</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 style={{height: 20}}></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>NAME</b></td>
|
<td><b>NAME</b></td>
|
||||||
|
@ -89,6 +97,7 @@ function Page(props: PageProps): JSX.Element {
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
|
<canvas id="canvas" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"next-i18next": "^8.5.1",
|
"next-i18next": "^8.5.1",
|
||||||
"next-seo": "^4.26.0",
|
"next-seo": "^4.26.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
|
"node-jose": "^2.0.0",
|
||||||
"node-pdf-verifier": "^1.0.1",
|
"node-pdf-verifier": "^1.0.1",
|
||||||
"pdfjs-dist": "^2.5.207",
|
"pdfjs-dist": "^2.5.207",
|
||||||
"pngjs": "^6.0.0",
|
"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": {
|
"node_modules/big.js": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
|
||||||
"integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw="
|
"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": {
|
"node_modules/escalade": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||||
|
@ -2709,6 +2723,11 @@
|
||||||
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
|
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
|
||||||
"dev": true
|
"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": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
@ -3061,6 +3080,31 @@
|
||||||
"he": "1.2.0"
|
"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": {
|
"node_modules/node-libs-browser": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
"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": {
|
"big.js": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
|
||||||
"integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw="
|
"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": {
|
"escalade": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||||
|
@ -7412,6 +7466,11 @@
|
||||||
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
|
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
|
||||||
"dev": true
|
"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": {
|
"loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
@ -7673,6 +7732,29 @@
|
||||||
"he": "1.2.0"
|
"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": {
|
"node-libs-browser": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
"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-i18next": "^8.5.1",
|
||||||
"next-seo": "^4.26.0",
|
"next-seo": "^4.26.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
|
"node-jose": "^2.0.0",
|
||||||
"node-pdf-verifier": "^1.0.1",
|
"node-pdf-verifier": "^1.0.1",
|
||||||
"pdfjs-dist": "^2.5.207",
|
"pdfjs-dist": "^2.5.207",
|
||||||
"pngjs": "^6.0.0",
|
"pngjs": "^6.0.0",
|
||||||
|
@ -54,6 +55,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/pako": "^1.0.1",
|
"@types/pako": "^1.0.1",
|
||||||
|
"@types/pngjs": "^6.0.1",
|
||||||
"@types/react": "^17.0.11",
|
"@types/react": "^17.0.11",
|
||||||
"autoprefixer": "^10.0.4",
|
"autoprefixer": "^10.0.4",
|
||||||
"postcss": "^8.1.10",
|
"postcss": "^8.1.10",
|
||||||
|
|
|
@ -74,10 +74,11 @@ function Index(): JSX.Element {
|
||||||
<Card content={
|
<Card content={
|
||||||
<div><p>{t('common:subtitle')}</p><br /><p>{t('common:subtitle2')}</p><br />
|
<div><p>{t('common:subtitle')}</p><br /><p>{t('common:subtitle2')}</p><br />
|
||||||
<b>{displayPassCount}</b><br/><br/>
|
<b>{displayPassCount}</b><br/><br/>
|
||||||
Sept 29 afternoon update:
|
Oct 1 morning update:
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<ul className="list-decimal list-outside" style={{ marginLeft: '20px' }}>
|
<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>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>
|
<li>System reminders (e.g. unsupported browsers) are now on the top to improve ease of use</li>
|
||||||
</ul><br />
|
</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
|
// adapted from https://github.com/fproulx/shc-covid19-decoder/blob/main/src/shc.js
|
||||||
// and https://github.com/ehn-dcc-development/dgc-check-mobile-app/blob/main/src/app/cose-js/sign.js
|
|
||||||
|
|
||||||
import base45 from 'base45';
|
const jsQR = require("jsqr");
|
||||||
import pako from 'pako';
|
const zlib = require("zlib");
|
||||||
import cbor from 'cbor-js';
|
import {Receipt, HashTable} from "./payload";
|
||||||
|
|
||||||
export function typedArrayToBufferSliced(array: Uint8Array): ArrayBuffer {
|
export function getQRFromImage(imageData) {
|
||||||
return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset);
|
return jsQR(
|
||||||
|
new Uint8ClampedArray(imageData.data.buffer),
|
||||||
|
imageData.width,
|
||||||
|
imageData.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
// vaccine codes based on Alex Dunae's findings
|
||||||
var buffer = new ArrayBuffer(array.length);
|
// https://gist.github.com/alexdunae/49cc0ea95001da3360ad6896fa5677ec
|
||||||
|
// http://mchp-appserv.cpe.umanitoba.ca/viewConcept.php?printer=Y&conceptID=1514
|
||||||
|
|
||||||
array.map(function (value, i) {
|
// .vc.credentialSubject.fhirBundle.entry
|
||||||
return buffer[i] = value;
|
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')) {
|
// console.log(decoded);
|
||||||
data = data.substring(3);
|
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(':')) {
|
let name = '';
|
||||||
data = data.substring(1);
|
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;
|
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
};
|
84
src/pass.ts
84
src/pass.ts
|
@ -5,26 +5,10 @@ import {Constants} from "./constants";
|
||||||
import {Payload, PayloadBody, PassDictionary} from "./payload";
|
import {Payload, PayloadBody, PassDictionary} from "./payload";
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { QRCodeMatrixUtil } from '@zxing/library';
|
import { QRCodeMatrixUtil } from '@zxing/library';
|
||||||
|
import {QrCode,Encoding,PackageResult,QrFormat,PassPhotoCommon} from './passphoto-common';
|
||||||
|
|
||||||
const crypto = require('crypto')
|
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 {
|
interface SignData {
|
||||||
PassJsonHash: string;
|
PassJsonHash: string;
|
||||||
useBlackVersion: boolean;
|
useBlackVersion: boolean;
|
||||||
|
@ -85,67 +69,12 @@ export class PassData {
|
||||||
|
|
||||||
// Create Payload
|
// Create Payload
|
||||||
try {
|
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
|
// Create pass data
|
||||||
const pass: PassData = new PassData(payload, qrCode);
|
|
||||||
|
const pass: PassData = new PassData(results.payload, results.qrCode);
|
||||||
|
|
||||||
// Create new zip
|
// Create new zip
|
||||||
const zip = [] as { path: string; data: Buffer | string }[];
|
const zip = [] as { path: string; data: Buffer | string }[];
|
||||||
|
@ -197,8 +126,7 @@ export class PassData {
|
||||||
|
|
||||||
return createZip(zip);
|
return createZip(zip);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Sentry.captureException(e);
|
return Promise.reject(e);
|
||||||
return Promise.reject();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
146
src/payload.ts
146
src/payload.ts
|
@ -1,5 +1,6 @@
|
||||||
import {Constants} from "./constants";
|
import {Constants} from "./constants";
|
||||||
import {COLORS} from "./colors";
|
import {COLORS} from "./colors";
|
||||||
|
import { TEXT_ALIGN } from "html2canvas/dist/types/css/property-descriptors/text-align";
|
||||||
|
|
||||||
export class Receipt {
|
export class Receipt {
|
||||||
constructor(public name: string, public vaccinationDate: string, public vaccineName: string, public dateOfBirth: string, public numDoses: number, public organization: string) {};
|
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 {
|
export class Payload {
|
||||||
|
|
||||||
receipt: Receipt;
|
receipts: HashTable<Receipt>;
|
||||||
rawData: string;
|
rawData: string;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
labelColor: string;
|
labelColor: string;
|
||||||
|
@ -48,10 +49,51 @@ export class Payload {
|
||||||
|
|
||||||
constructor(body: PayloadBody, numDose: number) {
|
constructor(body: PayloadBody, numDose: number) {
|
||||||
|
|
||||||
// Get name and date of birth information
|
let generic: PassDictionary = {
|
||||||
const name = body.receipts[numDose].name;
|
headerFields: [],
|
||||||
const dateOfBirth = body.receipts[numDose].dateOfBirth;
|
primaryFields: [],
|
||||||
const vaccineName = body.receipts[numDose].vaccineName;
|
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();
|
let vaccineNameProper = vaccineName.charAt(0) + vaccineName.substr(1).toLowerCase();
|
||||||
|
|
||||||
if (vaccineName.includes('PFIZER'))
|
if (vaccineName.includes('PFIZER'))
|
||||||
|
@ -63,42 +105,58 @@ export class Payload {
|
||||||
if (vaccineName.includes('ASTRAZENECA') || vaccineName.includes('COVISHIELD'))
|
if (vaccineName.includes('ASTRAZENECA') || vaccineName.includes('COVISHIELD'))
|
||||||
vaccineNameProper = 'AstraZeneca (Vaxzevria)'
|
vaccineNameProper = 'AstraZeneca (Vaxzevria)'
|
||||||
|
|
||||||
let doseVaccine = "#" + String(body.receipts[numDose].numDoses) + ": " + vaccineNameProper;
|
let doseVaccine = "#" + String(receipt.numDoses) + ": " + vaccineNameProper;
|
||||||
|
let fullyVaccinated = false;
|
||||||
|
|
||||||
if (name == undefined) {
|
if (receipt.numDoses > 1 ||
|
||||||
throw new Error('nameMissing');
|
vaccineName.toLowerCase().includes('janssen') ||
|
||||||
}
|
vaccineName.toLowerCase().includes('johnson') ||
|
||||||
if (dateOfBirth == undefined) {
|
vaccineName.toLowerCase().includes('j&j')) {
|
||||||
throw new Error('dobMissing');
|
fullyVaccinated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generic: PassDictionary = {
|
if (generic.primaryFields.length == 0) {
|
||||||
headerFields: [
|
generic.primaryFields.push(
|
||||||
],
|
|
||||||
primaryFields: [
|
|
||||||
{
|
{
|
||||||
key: "vaccine",
|
key: "vaccine",
|
||||||
label: "Vaccine",
|
label: "Vaccine",
|
||||||
value: doseVaccine,
|
value: doseVaccine
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
],
|
let fieldToPush = generic.secondaryFields;
|
||||||
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",
|
key: "issuer",
|
||||||
label: "Authorized Organization",
|
label: "Authorized Organization",
|
||||||
value: body.receipts[numDose].organization
|
value: receipt.organization
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "dov",
|
key: "dov",
|
||||||
label: "Date",
|
label: "Date",
|
||||||
value: body.receipts[numDose].vaccinationDate,
|
value: receipt.vaccinationDate,
|
||||||
// textAlignment: TextAlignment.right
|
|
||||||
}
|
}
|
||||||
],
|
);
|
||||||
auxiliaryFields: [
|
|
||||||
{
|
if (generic.auxiliaryFields.length == 0) {
|
||||||
|
generic.auxiliaryFields.push(
|
||||||
|
{
|
||||||
key: "name",
|
key: "name",
|
||||||
label: "Name",
|
label: "Name",
|
||||||
value: name
|
value: name
|
||||||
|
@ -107,33 +165,7 @@ export class Payload {
|
||||||
key: "dob",
|
key: "dob",
|
||||||
label: "Date of Birth",
|
label: "Date of Birth",
|
||||||
value: dateOfBirth
|
value: dateOfBirth
|
||||||
}
|
});
|
||||||
],
|
|
||||||
backFields: [
|
|
||||||
|
|
||||||
//TODO: add url link back to grassroots site
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
return fullyVaccinated;
|
||||||
// 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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
123
src/photo.ts
123
src/photo.ts
|
@ -4,25 +4,11 @@ import {v4 as uuid4} from 'uuid';
|
||||||
import {BrowserQRCodeSvgWriter} from "@zxing/browser";
|
import {BrowserQRCodeSvgWriter} from "@zxing/browser";
|
||||||
import { toPng, toJpeg, toBlob, toPixelData, toSvg } from 'html-to-image';
|
import { toPng, toJpeg, toBlob, toPixelData, toSvg } from 'html-to-image';
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
import {QrCode,Encoding,PackageResult,QrFormat,PassPhotoCommon} from './passphoto-common';
|
||||||
enum QrFormat {
|
import { EncodeHintType } from "@zxing/library";
|
||||||
PKBarcodeFormatQR = 'PKBarcodeFormatQR',
|
|
||||||
PKBarcodeFormatPDF417 = 'PKBarcodeFormatPDF417'
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Encoding {
|
|
||||||
utf8 = "utf-8",
|
|
||||||
iso88591 = "iso-8859-1"
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QrCode {
|
|
||||||
message: string;
|
|
||||||
format: QrFormat;
|
|
||||||
messageEncoding: Encoding;
|
|
||||||
// altText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Photo {
|
export class Photo {
|
||||||
|
|
||||||
logoText: string = Constants.NAME;
|
logoText: string = Constants.NAME;
|
||||||
organizationName: string = Constants.NAME;
|
organizationName: string = Constants.NAME;
|
||||||
description: string = Constants.NAME;
|
description: string = Constants.NAME;
|
||||||
|
@ -33,99 +19,62 @@ export class Photo {
|
||||||
barcodes: Array<QrCode>;
|
barcodes: Array<QrCode>;
|
||||||
barcode: QrCode;
|
barcode: QrCode;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static async generatePass(payloadBody: PayloadBody, numDose: number): Promise<Blob> {
|
static async generatePass(payloadBody: PayloadBody, numDose: number): Promise<Blob> {
|
||||||
|
|
||||||
// Create Payload
|
// Create Payload
|
||||||
try {
|
try {
|
||||||
const payload: Payload = new Payload(payloadBody, numDose);
|
console.log('generatePass');
|
||||||
|
const results = await PassPhotoCommon.preparePayload(payloadBody, numDose);
|
||||||
|
|
||||||
payload.serialNumber = uuid4();
|
const payload = results.payload;
|
||||||
|
const qrCode = results.qrCode;
|
||||||
|
|
||||||
// register record
|
let receipt;
|
||||||
|
if (results.payload.rawData.length == 0) {
|
||||||
const clonedReceipt = Object.assign({}, payload.receipt);
|
receipt = results.payload.receipts[numDose];
|
||||||
delete clonedReceipt.name;
|
} else {
|
||||||
delete clonedReceipt.dateOfBirth;
|
receipt = results.payload.receipts[numDose];
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
const body = document.getElementById('pass-image');
|
||||||
body.hidden = false;
|
body.hidden = false;
|
||||||
body.style.backgroundColor = payload.backgroundColor
|
body.style.backgroundColor = payload.backgroundColor
|
||||||
|
|
||||||
const name = payload.receipt.name;
|
const vaccineName = receipt.vaccineName;
|
||||||
const dateOfBirth = payload.receipt.dateOfBirth;
|
let doseVaccine = "#" + String(receipt.numDoses) + ": " + vaccineName;
|
||||||
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;
|
|
||||||
|
|
||||||
document.getElementById('vaccineName').innerText = doseVaccine;
|
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 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');
|
svg.setAttribute('style','background-color: white');
|
||||||
document.getElementById('qrcode').appendChild(svg);
|
document.getElementById('qrcode').appendChild(svg);
|
||||||
|
|
||||||
const blobPromise = toBlob(body);
|
const blobPromise = toBlob(body);
|
||||||
return blobPromise;
|
return blobPromise;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Sentry.captureException(e);
|
return Promise.reject(e);
|
||||||
return Promise.reject();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
126
src/process.ts
126
src/process.ts
|
@ -1,10 +1,14 @@
|
||||||
import {PayloadBody, Receipt, HashTable} from "./payload";
|
import {PayloadBody, Receipt, HashTable} from "./payload";
|
||||||
import * as PdfJS from 'pdfjs-dist'
|
import * as PdfJS from 'pdfjs-dist'
|
||||||
import {COLORS} from "./colors";
|
import jsQR, {QRCode} from "jsqr";
|
||||||
import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6
|
import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6
|
||||||
|
import {COLORS} from "./colors";
|
||||||
import * as Sentry from '@sentry/react';
|
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 {PNG} from 'pngjs'
|
||||||
// import {decodeData} from "./decode";
|
// 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`
|
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
|
// Read file
|
||||||
const fileBuffer = await file.arrayBuffer();
|
const fileBuffer = await file.arrayBuffer();
|
||||||
|
|
||||||
let receipts: HashTable<Receipt>;
|
let receipts: HashTable<Receipt>;
|
||||||
|
let rawData = ''; // unused at the moment, the original use was to store the QR code from issuer
|
||||||
|
|
||||||
switch (file.type) {
|
switch (file.type) {
|
||||||
case 'application/pdf':
|
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
|
break
|
||||||
default:
|
default:
|
||||||
throw Error('invalidFileType')
|
throw Error('invalidFileType')
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawData = ''; // unused at the moment, the original use was to store the QR code from issuer
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
receipts: receipts,
|
receipts: receipts,
|
||||||
rawData: rawData
|
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 {
|
try {
|
||||||
|
|
||||||
const certs = getCertificatesInfoFromPDF(signedPdfBuffer);
|
const certs = getCertificatesInfoFromPDF(fileBuffer);
|
||||||
|
|
||||||
const result = certs[0];
|
const result = certs[0];
|
||||||
const refcert = '-----BEGIN CERTIFICATE-----\r\n'+
|
const refcert = '-----BEGIN CERTIFICATE-----\r\n'+
|
||||||
|
@ -95,7 +133,7 @@ async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise<HashTable<Receipt
|
||||||
|
|
||||||
if (( issuedpemCertificate )) {
|
if (( issuedpemCertificate )) {
|
||||||
//console.log('getting receipt details inside PDF');
|
//console.log('getting receipt details inside PDF');
|
||||||
const receipt = await getPdfDetails(signedPdfBuffer);
|
const receipt = await getPdfDetails(fileBuffer);
|
||||||
// console.log(JSON.stringify(receipt, null, 2));
|
// console.log(JSON.stringify(receipt, null, 2));
|
||||||
return Promise.resolve(receipt);
|
return Promise.resolve(receipt);
|
||||||
|
|
||||||
|
@ -111,12 +149,15 @@ async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise<HashTable<Receipt
|
||||||
return Promise.reject(`invalid certificate + ${JSON.stringify(result)}`);
|
return Promise.reject(`invalid certificate + ${JSON.stringify(result)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
Sentry.captureException(e);
|
|
||||||
|
|
||||||
if (e.message.includes('Failed to locate ByteRange')) {
|
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.'
|
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);
|
return Promise.reject(e);
|
||||||
}
|
}
|
||||||
|
@ -159,6 +200,7 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt
|
||||||
numDoses = Number(value.split(' ')[3]);
|
numDoses = Number(value.split(' ')[3]);
|
||||||
}
|
}
|
||||||
receiptObj[numDoses] = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, numDoses, organization);
|
receiptObj[numDoses] = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, numDoses, organization);
|
||||||
|
console.log(receiptObj[numDoses]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(receiptObj);
|
return Promise.resolve(receiptObj);
|
||||||
|
@ -167,3 +209,65 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt
|
||||||
return Promise.reject(e);
|
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: [
|
integrations: [
|
||||||
new Integrations.BrowserTracing(),
|
new Integrations.BrowserTracing(),
|
||||||
],
|
],
|
||||||
attachStacktrace: true
|
attachStacktrace: true,
|
||||||
|
tracesSampleRate: 0.5
|
||||||
|
|
||||||
});
|
});
|
||||||
console.log('sentry initialized');
|
console.log('sentry initialized');
|
||||||
|
|
||||||
|
|
|
@ -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…
Reference in New Issue