Merge remote-tracking branch 'origin/main' into improve-logging-when-not-digitally-signed
* origin/main: (22 commits) Fix typo in expiry date (10-23 -> 10-22), bump page rev name fix for photo receipts updated save photo headings and added expiry Fix for pdfjs type imports tuned the dates heading / details a bit. added expiry date to pass added expiration date and default to ON logic Fix input element not accepting PDFs on older iOS devices Fix Save Photo not working on iOS due to html-to-image error when handling on that platform. cleaned up console logging to be ready for main branch merge pre-merge with main updates Added Saskatchewan SHC issuer info Stop splitting vaccinename so we can detect AZ correctly special handling of shc code from QC specify error correction level of QR code in photos to make it compatible with BC scanner app functional shc (including photo) add to wallet working now - working on photos portion verifyJWS working now... picked up latest issuer code SHC - Smart Health Card support. Parse QR code from PNG image and parse it into Payload Add support for iPhone 6 iOS 12.5 ... # Conflicts: # src/process.ts
This commit is contained in:
commit
cdf3748317
|
@ -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 {
|
|||
|
||||
<input type='file'
|
||||
id='file'
|
||||
accept="application/pdf"
|
||||
accept="application/pdf,.png"
|
||||
ref={inputFile}
|
||||
style={{display: 'none'}}
|
||||
/>
|
||||
|
|
|
@ -36,7 +36,7 @@ function Page(props: PageProps): JSX.Element {
|
|||
<a href="https://github.com/billylo1/covidpass" className="underline">{t('common:gitHub')}</a>
|
||||
<a href="https://vaccine-ontario.ca" className="underline">{t('common:returnToMainSite')}</a>
|
||||
</nav>
|
||||
<div className="flex pt-4 flex-row space-x-4 justify-center text-md flex-wrap">Last updated: 2021-09-29 (v1.9.12)</div>
|
||||
<div className="flex pt-4 flex-row space-x-4 justify-center text-md flex-wrap">Last updated: 2021-10-05 (v1.9.15)</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
@ -48,14 +48,12 @@ function Page(props: PageProps): JSX.Element {
|
|||
<tbody>
|
||||
<tr>
|
||||
<td><img src='shield4.svg' width='50' height='50' /></td>
|
||||
<td style={{fontSize: 20, width: 280}}><span><b> Vaccination Receipt</b></span></td>
|
||||
<td style={{fontSize: 20, width: 280}}><span style={{marginLeft: '11px'}}><b>Vaccination Receipt</b></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div style={{height:12}}><b>VACCINE</b></div>
|
||||
<div id='vaccineName' style={{fontSize:28}}></div>
|
||||
<br/>
|
||||
|
@ -64,12 +62,20 @@ function Page(props: PageProps): JSX.Element {
|
|||
<tbody>
|
||||
<tr>
|
||||
<td style={{width: 220}}><b>AUTHORIZED ORGANIZATION</b></td>
|
||||
<td><b>DATE</b></td>
|
||||
<td><b>VACC. DATE</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id='organization' style={{width: 220}}></td>
|
||||
<td id='vaccinationDate' style={{width:120}}></td>
|
||||
</tr>
|
||||
<tr id='extraRow2' hidden>
|
||||
<td id='organization2' style={{width: 220}}></td>
|
||||
<td id='vaccinationDate2' style={{width:120}}></td>
|
||||
</tr>
|
||||
<tr id='extraRow1' hidden>
|
||||
<td id='organization1' style={{width: 220}}></td>
|
||||
<td id='vaccinationDate1' style={{width:120}}></td>
|
||||
</tr>
|
||||
<tr style={{height: 20}}></tr>
|
||||
<tr>
|
||||
<td><b>NAME</b></td>
|
||||
|
@ -79,16 +85,24 @@ function Page(props: PageProps): JSX.Element {
|
|||
<td id='name' style={{fontSize: 12}}></td>
|
||||
<td id='dob' style={{fontSize: 12}}></td>
|
||||
</tr>
|
||||
<tr style={{height: 20}}></tr>
|
||||
<tr>
|
||||
<td><b></b></td>
|
||||
<td><b>QR CODE EXPIRY</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id='null' style={{fontSize: 12}}></td>
|
||||
<td id='expiry' style={{fontSize: 12}}>2021-10-22</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<div id='qrcode' style={{width:'63%', display:'block', marginLeft: 'auto', marginRight: 'auto'}}></div>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
<canvas id="canvas" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -74,12 +74,12 @@ function Index(): JSX.Element {
|
|||
<Card content={
|
||||
<div><p>{t('common:subtitle')}</p><br /><p>{t('common:subtitle2')}</p><br />
|
||||
<b>{displayPassCount}</b><br/><br/>
|
||||
Sept 29 afternoon update:
|
||||
Oct 3 evening update:
|
||||
<br />
|
||||
<br />
|
||||
<ul className="list-decimal list-outside" style={{ marginLeft: '20px' }}>
|
||||
<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>Added expiration date to Apple Wallet pass so it aligns with the province's schedule.</li>
|
||||
<li>On Oct 22, we will update this tool as well so you can import the official QR code into your mobile wallet too.</li>
|
||||
</ul><br />
|
||||
<p>{t('common:continueSpirit')}</p>
|
||||
<br />
|
||||
|
|
|
@ -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 ❤️.
|
||||
|
|
129
src/decode.ts
129
src/decode.ts
|
@ -1,48 +1,107 @@
|
|||
// Taken from https://github.com/ehn-dcc-development/ehn-sign-verify-javascript-trivial/blob/main/cose_verify.js
|
||||
// and https://github.com/ehn-dcc-development/dgc-check-mobile-app/blob/main/src/app/cose-js/sign.js
|
||||
// adapted from https://github.com/fproulx/shc-covid19-decoder/blob/main/src/shc.js
|
||||
|
||||
import base45 from 'base45';
|
||||
import pako from 'pako';
|
||||
import cbor from 'cbor-js';
|
||||
const jsQR = require("jsqr");
|
||||
const zlib = require("zlib");
|
||||
import {Receipt, HashTable} from "./payload";
|
||||
|
||||
export function typedArrayToBufferSliced(array: Uint8Array): ArrayBuffer {
|
||||
return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset);
|
||||
export function getQRFromImage(imageData) {
|
||||
return jsQR(
|
||||
new Uint8ClampedArray(imageData.data.buffer),
|
||||
imageData.width,
|
||||
imageData.height
|
||||
);
|
||||
}
|
||||
|
||||
export function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
||||
var buffer = new ArrayBuffer(array.length);
|
||||
// vaccine codes based on Alex Dunae's findings
|
||||
// https://gist.github.com/alexdunae/49cc0ea95001da3360ad6896fa5677ec
|
||||
// http://mchp-appserv.cpe.umanitoba.ca/viewConcept.php?printer=Y&conceptID=1514
|
||||
|
||||
array.map(function (value, i) {
|
||||
return buffer[i] = value;
|
||||
})
|
||||
// .vc.credentialSubject.fhirBundle.entry
|
||||
export function decodedStringToReceipt(decoded: object) : HashTable<Receipt> {
|
||||
|
||||
return array.buffer;
|
||||
}
|
||||
const codeToVaccineName = {
|
||||
'28581000087106': 'PFIZER',
|
||||
'28951000087107': 'JANSSEN',
|
||||
'28761000087108': 'ASTRAZENECA',
|
||||
'28571000087109': 'MODERNA'
|
||||
}
|
||||
|
||||
export function decodeData(data: string): Object {
|
||||
const cvxCodeToVaccineName = { // https://www2a.cdc.gov/vaccines/iis/iisstandards/vaccines.asp?rpt=cvx
|
||||
'208': 'PFIZER',
|
||||
'212': 'JANSSEN',
|
||||
'210': 'ASTRAZENECA',
|
||||
'207': 'MODERNA'
|
||||
}
|
||||
|
||||
if (data.startsWith('HC1')) {
|
||||
data = data.substring(3);
|
||||
// console.log(decoded);
|
||||
const shcResources = decoded['vc'].credentialSubject.fhirBundle.entry;
|
||||
let issuer;
|
||||
if (decoded['iss'].includes('quebec.ca')) {
|
||||
issuer = 'qc';
|
||||
}
|
||||
if (decoded['iss'].includes('ontariohealth.ca')) {
|
||||
issuer = 'on';
|
||||
}
|
||||
if (decoded['iss'] == "https://smarthealthcard.phsa.ca/v1/issuer") {
|
||||
issuer = 'bc';
|
||||
}
|
||||
|
||||
if (data.startsWith(':')) {
|
||||
data = data.substring(1);
|
||||
let name = '';
|
||||
let dateOfBirth;
|
||||
let receipts : HashTable<Receipt> = {};
|
||||
|
||||
const numResources = shcResources.length;
|
||||
for (let i = 0; i < numResources; i++) {
|
||||
const resource = shcResources[i]['resource'];
|
||||
if (resource["resourceType"] == 'Patient') {
|
||||
if (name.length > 0)
|
||||
name += '\n';
|
||||
|
||||
for (const nameField of resource.name) {
|
||||
for (const given of nameField.given) {
|
||||
name += (given + ' ')
|
||||
}
|
||||
name += (nameField.family);
|
||||
}
|
||||
dateOfBirth = resource['birthDate'];
|
||||
}
|
||||
if (resource["resourceType"] == 'Immunization') {
|
||||
let vaccineName : string;
|
||||
let organizationName : string;
|
||||
let vaccinationDate : string;
|
||||
for (const vaccineCodes of resource.vaccineCode.coding) {
|
||||
if (vaccineCodes.system.includes("snomed.info")) { //bc
|
||||
vaccineName = codeToVaccineName[vaccineCodes.code];
|
||||
if (vaccineName == undefined)
|
||||
vaccineName = 'Unknown - ' + vaccineCodes.code;
|
||||
} else if (vaccineCodes.system == "http://hl7.org/fhir/sid/cvx") { //qc
|
||||
vaccineName = cvxCodeToVaccineName[vaccineCodes.code];
|
||||
if (vaccineName == undefined)
|
||||
vaccineName = 'Unknown - ' + vaccineCodes.code;
|
||||
}
|
||||
}
|
||||
|
||||
let performers = resource['performer']; // BC
|
||||
let receiptNumber;
|
||||
if (issuer == 'bc') {
|
||||
performers = resource['performer'];
|
||||
receiptNumber = shcResources[i]['fullUrl'].split(':')[1];
|
||||
for (let j = 0; j < performers.length; j++) {
|
||||
const performer = performers[j];
|
||||
organizationName = performer.actor.display;
|
||||
}
|
||||
}
|
||||
if (issuer == 'qc') {
|
||||
organizationName = resource['location'].display; // QC
|
||||
receiptNumber = resource['protocolApplied'].doseNumber;
|
||||
}
|
||||
vaccinationDate = resource.occurrenceDateTime;
|
||||
|
||||
const receipt = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, receiptNumber, organizationName);
|
||||
// console.log(receipt);
|
||||
receipts[receiptNumber] = receipt;
|
||||
}
|
||||
}
|
||||
return receipts;
|
||||
|
||||
var arrayBuffer: Uint8Array = base45.decode(data);
|
||||
|
||||
if (arrayBuffer[0] == 0x78) {
|
||||
arrayBuffer = pako.inflate(arrayBuffer);
|
||||
}
|
||||
|
||||
var payloadArray: Array<Uint8Array> = cbor.decode(typedArrayToBuffer(arrayBuffer));
|
||||
|
||||
if (!Array.isArray(payloadArray) || payloadArray.length !== 4) {
|
||||
throw new Error('decodingFailed');
|
||||
}
|
||||
|
||||
var plaintext: Uint8Array = payloadArray[2];
|
||||
var decoded: Object = cbor.decode(typedArrayToBufferSliced(plaintext));
|
||||
|
||||
return decoded;
|
||||
}
|
|
@ -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,
|
||||
};
|
88
src/pass.ts
88
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<QrCode>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
||||
}
|
||||
}
|
160
src/payload.ts
160
src/payload.ts
|
@ -1,5 +1,6 @@
|
|||
import {Constants} from "./constants";
|
||||
import {COLORS} from "./colors";
|
||||
import { TEXT_ALIGN } from "html2canvas/dist/types/css/property-descriptors/text-align";
|
||||
|
||||
export class Receipt {
|
||||
constructor(public name: string, public vaccinationDate: string, public vaccineName: string, public dateOfBirth: string, public numDoses: number, public organization: string) {};
|
||||
|
@ -36,7 +37,7 @@ export interface PayloadBody {
|
|||
|
||||
export class Payload {
|
||||
|
||||
receipt: Receipt;
|
||||
receipts: HashTable<Receipt>;
|
||||
rawData: string;
|
||||
backgroundColor: string;
|
||||
labelColor: string;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
124
src/photo.ts
124
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<QrCode>;
|
||||
barcode: QrCode;
|
||||
|
||||
|
||||
|
||||
static async generatePass(payloadBody: PayloadBody, numDose: number): Promise<Blob> {
|
||||
|
||||
// Create Payload
|
||||
try {
|
||||
const payload: Payload = new Payload(payloadBody, numDose);
|
||||
console.log('generatePass');
|
||||
const results = await PassPhotoCommon.preparePayload(payloadBody, numDose);
|
||||
|
||||
const payload = results.payload;
|
||||
const qrCode = results.qrCode;
|
||||
|
||||
payload.serialNumber = uuid4();
|
||||
|
||||
// register record
|
||||
|
||||
const clonedReceipt = Object.assign({}, payload.receipt);
|
||||
delete clonedReceipt.name;
|
||||
delete clonedReceipt.dateOfBirth;
|
||||
clonedReceipt["serialNumber"] = payload.serialNumber;
|
||||
clonedReceipt["type"] = 'photo';
|
||||
|
||||
let requestOptions = {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(clonedReceipt) // body data type must match "Content-Type" header
|
||||
let receipt;
|
||||
if (results.payload.rawData.length == 0) {
|
||||
receipt = results.payload.receipts[numDose];
|
||||
} else {
|
||||
receipt = results.payload.receipts[numDose];
|
||||
}
|
||||
|
||||
console.log('registering ' + JSON.stringify(clonedReceipt, null, 2));
|
||||
const configResponse = await fetch('/api/config')
|
||||
const verifierHost = (await configResponse.json()).verifierHost
|
||||
|
||||
// const verifierHost = 'https://verifier.vaccine-ontario.ca';
|
||||
|
||||
const response = await fetch('https://us-central1-grassroot-verifier.cloudfunctions.net/register', requestOptions);
|
||||
const responseJson = await response.json();
|
||||
|
||||
console.log(JSON.stringify(responseJson,null,2));
|
||||
|
||||
if (responseJson["result"] != 'OK')
|
||||
return Promise.reject();
|
||||
|
||||
const encodedUri = `serialNumber=${encodeURIComponent(payload.serialNumber)}&vaccineName=${encodeURIComponent(payload.receipt.vaccineName)}&vaccinationDate=${encodeURIComponent(payload.receipt.vaccinationDate)}&organization=${encodeURIComponent(payload.receipt.organization)}&dose=${encodeURIComponent(payload.receipt.numDoses)}`;
|
||||
const qrCodeUrl = `${verifierHost}/verify?${encodedUri}`;
|
||||
|
||||
// Create QR Code Object
|
||||
const qrCode: QrCode = {
|
||||
message: qrCodeUrl,
|
||||
format: QrFormat.PKBarcodeFormatQR,
|
||||
messageEncoding: Encoding.iso88591,
|
||||
// altText : payload.rawData
|
||||
|
||||
}
|
||||
|
||||
// Create photo
|
||||
// const photo: Photo = new Photo(payload, qrCode);
|
||||
|
||||
// const body = domTree.getElementById('main');
|
||||
const body = document.getElementById('pass-image');
|
||||
body.hidden = false;
|
||||
body.style.backgroundColor = payload.backgroundColor
|
||||
|
||||
const name = payload.receipt.name;
|
||||
const dateOfBirth = payload.receipt.dateOfBirth;
|
||||
const vaccineName = payload.receipt.vaccineName;
|
||||
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<EncodeHintType,any> = new Map().set(EncodeHintType.ERROR_CORRECTION,'L');
|
||||
const svg = codeWriter.write(qrCode.message,200,200, hints);
|
||||
svg.setAttribute('style','background-color: white');
|
||||
document.getElementById('qrcode').appendChild(svg);
|
||||
|
||||
const blobPromise = toBlob(body);
|
||||
return blobPromise;
|
||||
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return Promise.reject();
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
138
src/process.ts
138
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<PayloadBody> {
|
||||
export async function getPayloadBodyFromFile(file: File): Promise<PayloadBody> {
|
||||
// Read file
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
let receipts: HashTable<Receipt>;
|
||||
let rawData = ''; // unused at the moment, the original use was to store the QR code from issuer
|
||||
|
||||
switch (file.type) {
|
||||
case 'application/pdf':
|
||||
receipts = await loadPDF(fileBuffer)
|
||||
const receiptType = await detectReceiptType(fileBuffer);
|
||||
console.log(`receiptType = ${receiptType}`);
|
||||
if (receiptType == 'ON') {
|
||||
receipts = await loadPDF(fileBuffer) // receipt type is needed to decide if digital signature checking is needed
|
||||
} else {
|
||||
const shcData = await processSHC(fileBuffer);
|
||||
receipts = shcData.receipts;
|
||||
rawData = shcData.rawData;
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw Error('invalidFileType')
|
||||
}
|
||||
|
||||
const rawData = ''; // unused at the moment, the original use was to store the QR code from issuer
|
||||
|
||||
return {
|
||||
receipts: receipts,
|
||||
rawData: rawData
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise<HashTable<Receipt>> {
|
||||
async function detectReceiptType(fileBuffer : ArrayBuffer): Promise<string> {
|
||||
|
||||
// Ontario has 'COVID-19 vaccination receipt'
|
||||
// BC has BC Vaccine Card
|
||||
|
||||
console.log('detectPDFTypeAndProcess');
|
||||
|
||||
const typedArray = new Uint8Array(fileBuffer);
|
||||
let loadingTask = PdfJS.getDocument(typedArray);
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
const pdfPage = await pdfDocument.getPage(1); //first page
|
||||
const content = await pdfPage.getTextContent();
|
||||
const numItems = content.items.length;
|
||||
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<HashTable<Receipt>> {
|
||||
|
||||
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<HashTable<Receipt
|
|||
|
||||
if (( issuedpemCertificate )) {
|
||||
//console.log('getting receipt details inside PDF');
|
||||
const receipt = await getPdfDetails(signedPdfBuffer);
|
||||
const receipt = await getPdfDetails(fileBuffer);
|
||||
// console.log(JSON.stringify(receipt, null, 2));
|
||||
return Promise.resolve(receipt);
|
||||
|
||||
|
@ -110,19 +153,19 @@ async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise<HashTable<Receipt
|
|||
console.error('invalid certificate');
|
||||
return Promise.reject(`invalid certificate + ${JSON.stringify(result)}`);
|
||||
}
|
||||
|
||||
|
||||
} catch (e) {
|
||||
|
||||
console.error(e);
|
||||
|
||||
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.';
|
||||
Sentry.captureMessage("Attempted to generate a pass from a PDF that was not digitally signed.");
|
||||
e.message = 'Sorry. Selected PDF file is not digitally signed. Please download official copy from Step 1 and retry. Thanks.'
|
||||
} else {
|
||||
console.error(e);
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt>> {
|
||||
|
@ -153,7 +196,6 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt
|
|||
}
|
||||
if (value.includes('Product name')) {
|
||||
vaccineName = (content.items[i+1] as TextItem).str;
|
||||
vaccineName = vaccineName.split(' ')[0];
|
||||
}
|
||||
if (value.includes('Date of birth'))
|
||||
dateOfBirth = (content.items[i+1] as TextItem).str;
|
||||
|
@ -163,6 +205,7 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt
|
|||
numDoses = Number(value.split(' ')[3]);
|
||||
}
|
||||
receiptObj[numDoses] = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, numDoses, organization);
|
||||
console.log(receiptObj[numDoses]);
|
||||
}
|
||||
|
||||
return Promise.resolve(receiptObj);
|
||||
|
@ -170,5 +213,66 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt
|
|||
Sentry.captureException(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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
48
yarn.lock
48
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"
|
||||
|
|
Loading…
Reference in New Issue