v01 - initial push

working code - read pdf, get PDF signer cert details, validate and payload inside receipt (name, vaccinationDate, vaccineType, date of birth and number of doses received)
This commit is contained in:
Billy Lo 2021-08-23 22:33:48 -04:00
parent de82c494ad
commit 0d5bfd9203
9 changed files with 11003 additions and 2387 deletions

View File

@ -168,12 +168,12 @@ function Form(): JSX.Element {
<div className="space-y-5">
<p>{t('index:selectCertificateDescription')}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<button
{/* <button
type="button"
onClick={isCameraOpen ? hideCameraView : showCameraView}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
{isCameraOpen ? t('index:stopCamera') : t('index:startCamera')}
</button>
</button> */}
<button
type="button"
onClick={showFileDialog}
@ -209,15 +209,15 @@ function Form(): JSX.Element {
}
</div>
}/>
<Card step="2" heading={t('index:pickColor')} content={
{/* <Card step="2" heading={t('index:pickColor')} content={
<div className="space-y-5">
<p>{t('index:pickColorDescription')}</p>
<div className="relative inline-block w-full">
<Colors onChange={setSelectedColor} initialValue={selectedColor}/>
</div>
</div>
}/>
<Card step="3" heading={t('index:addToWallet')} content={
}/> */}
<Card step="2" heading={t('index:addToWallet')} content={
<div className="space-y-5">
<p>
{t('index:dataPrivacyDescription')}
@ -231,10 +231,10 @@ function Form(): JSX.Element {
<ul className="list-none">
<Check text={t('createdOnDevice')}/>
<Check text={t('openSourceTransparent')}/>
<Check text={t('hostedInEU')}/>
{/* <Check text={t('hostedInEU')}/> */}
</ul>
</div>
<label htmlFor="privacy" className="flex flex-row space-x-4 items-center pb-2">
{/* <label htmlFor="privacy" className="flex flex-row space-x-4 items-center pb-2">
<input type="checkbox" id="privacy" value="privacy" required className="h-5 w-5 outline-none"/>
<p>
{t('index:iAcceptThe')}&nbsp;
@ -244,7 +244,7 @@ function Form(): JSX.Element {
</a>
</Link>.
</p>
</label>
</label> */}
<div className="flex flex-row items-center justify-start">
<button id="download" type="submit"
className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">

View File

@ -25,10 +25,10 @@ function Page(props: PageProps): JSX.Element {
<footer>
<nav className="nav flex pt-4 flex-row space-x-4 justify-center text-md font-bold flex-wrap">
<a href="https://ko-fi.com/marvinsxtr" className="hover:underline">{t('common:donate')}</a>
<a href="https://github.com/marvinsxtr/covidpass" className="hover:underline">{t('common:gitHub')}</a>
<Link href="/privacy"><a className="hover:underline">{t('common:privacyPolicy')}</a></Link>
<Link href="/imprint"><a className="hover:underline">{t('common:imprint')}</a></Link>
{/* <a href="https://ko-fi.com/marvinsxtr" className="hover:underline">{t('common:donate')}</a> */}
<a href="https://github.com/billylo1/covidpass" className="hover:underline">{t('common:gitHub')}</a>
{/* <Link href="/privacy"><a className="hover:underline">{t('common:privacyPolicy')}</a></Link>
<Link href="/imprint"><a className="hover:underline">{t('common:imprint')}</a></Link> */}
</nav>
</footer>
</main>

8455
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,14 @@
"build": "next build",
"start": "next start"
},
"browser": {
"fs": false,
"os": false,
"path": false
},
"dependencies": {
"@headlessui/react": "^1.3.0",
"@ninja-labs/verify-pdf": "^0.3.9",
"@zxing/browser": "^0.0.9",
"@zxing/library": "^0.18.6",
"base45": "^3.0.0",
@ -18,7 +24,7 @@
"file-saver": "^2.0.5",
"jpeg-js": "^0.4.3",
"jsqr": "^1.4.0",
"next": "latest",
"next": "^11.0.1",
"next-i18next": "^8.5.1",
"next-seo": "^4.26.0",
"node-fetch": "^2.6.1",
@ -26,6 +32,7 @@
"pngjs": "^6.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"tls": "^0.0.1",
"uuid": "^8.3.2",
"webpack": "^5.0.0",
"worker-loader": "^3.0.7"

View File

@ -9,8 +9,8 @@ import Page from '../components/Page';
function Index(): JSX.Element {
const { t } = useTranslation(['common', 'index', 'errors']);
const title = 'CovidPass';
const description = 'Add your EU Digital COVID Certificates to your favorite wallet app.';
const title = 'Ontario Vaccination Record (PDF -> Apple Wallet / Android Wallet)';
const description = 'Add your Ontario vaccination receipt to your Apple / Android wallet.';
return (
<>
@ -18,7 +18,7 @@ function Index(): JSX.Element {
title={title}
description={description}
openGraph={{
url: 'https://covidpass.marvinsextro.de/',
url: 'https://receipt2wallet.vaccine-ontario.ca/',
title: title,
description: description,
images: [
@ -32,8 +32,8 @@ function Index(): JSX.Element {
site_name: title,
}}
twitter={{
handle: '@marvinsxtr',
site: '@marvinsxtr',
handle: '@vaxtoronto',
site: '@vaxtotorono',
cardType: 'summary_large_image',
}}
/>

View File

@ -1,5 +1,5 @@
title: CovidPass
subtitle: Add your EU Digital COVID Certificates to your favorite wallet apps.
title: Ontario vaccination receipt to mobile wallet
subtitle: This tool validates the digital signature of your receipt and saves the information onto your mobile wallet for easy validation.
privacyPolicy: Privacy Policy
donate: Sponsor
gitHub: GitHub

View File

@ -1,13 +1,12 @@
iosHint: On iOS, please use the Safari Browser.
errorClose: Close
selectCertificate: Select Certificate
selectCertificate: Select vaccination receipt (PDF)
selectCertificateDescription: |
Please scan the QR code on your certificate or select a screenshot or PDF page with the QR code.
Note that selecting a file directly from camera is not supported.
stopCamera: Stop Camera
startCamera: Start Camera
If you have more than one receipts, just select the most recent one downloaded from covid19.ontariohealth.ca
#stopCamera: Stop Camera
#startCamera: Start Camera
openFile: Select File
foundQrCode: Found QR Code!
#foundQrCode: Found QR Code!
pickColor: Pick a Color
pickColorDescription: Pick a background color for your pass.
colorWhite: white
@ -20,10 +19,9 @@ colorPurple: purple
colorTeal: teal
addToWallet: Add to Wallet
dataPrivacyDescription: |
Data privacy is of special importance when processing health-related data.
In order for you to make an informed decision, please read the
Your vaccination receipt is processed on your mobile phone only. No data is sent to the internet.
iAcceptThe: I accept the
privacyPolicy: Privacy Policy
createdOnDevice: Created on your device
openSourceTransparent: Open source and transparent
hostedInEU: Hosted in the EU
openSourceTransparent: 100% open source - You can validate all lines of code used.
#hostedInEU: Hosted in the EU

View File

@ -5,9 +5,15 @@ import jsQR, {QRCode} from "jsqr";
import {decodeData} from "./decode";
import {Result} from "@zxing/library";
import {COLORS} from "./colors";
import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6
import verifyPDF from "@ninja-labs/verify-pdf";
PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PdfJS.version}/pdf.worker.js`
class Receipt {
constructor(public name: string, public vaccinationDate: string, public vaccineName: string, public dateOfBirth: string, public numDoses: number) {};
}
export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise<PayloadBody> {
// Read file
const fileBuffer = await file.arrayBuffer();
@ -17,12 +23,12 @@ export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise
switch (file.type) {
case 'application/pdf':
console.log('pdf')
imageData = await getImageDataFromPdf(fileBuffer)
break
case 'image/png':
console.log('png')
imageData = await getImageDataFromPng(fileBuffer)
await loadPDF(fileBuffer)
break
// case 'image/png':
// console.log('png')
// imageData = await getImageDataFromPng(fileBuffer)
// break
default:
throw Error('invalidFileType')
}
@ -126,4 +132,74 @@ async function getImageDataFromPdf(fileBuffer: ArrayBuffer): Promise<ImageData>
// Return PDF Image Data
return canvasContext.getImageData(0, 0, canvas.width, canvas.height)
}
}
async function loadPDF(signedPdfBuffer): Promise<any> {
try {
const certs = getCertificatesInfoFromPDF(signedPdfBuffer);
// console.log('certs = ' + JSON.stringify(certs, null, 2));
// check signature
// console.log("verifying");
// const verificationResult = verifyPDF(signedPdfBuffer); // not sure why it failed
const result = certs[0];
const isClientCertificate = result.clientCertificate;
const issuedByEntrust = (result.issuedBy.organizationName == 'Entrust, Inc.');
const issuedToOntarioHealth = (result.issuedTo.commonName == 'covid19signer.ontariohealth.ca');
if (isClientCertificate && issuedByEntrust && issuedToOntarioHealth) {
console.log('valid, getting payload');
const receipt = await getPdfDetails(signedPdfBuffer);
console.log(JSON.stringify(receipt, null, 2));
} else {
console.error('invalid certificate');
return Promise.reject('invalid certificate');
}
} catch (e) {
console.error(e);
return Promise.reject(e);
}
}
async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<Receipt> {
const typedArray = new Uint8Array(fileBuffer);
let loadingTask = PdfJS.getDocument(typedArray);
const pdfDocument = await loadingTask.promise;
// Load last PDF page
const pageNumber = pdfDocument.numPages;
const pdfPage = await pdfDocument.getPage(pageNumber);
const content = await pdfPage.getTextContent();
const numItems = content.items.length;
let name, vaccinationDate, vaccineName, dateOfBirth, numDoses;
for (let i = 0; i < numItems; i++) {
const item = content.items[i];
const value = item.str;
if (value.includes('Name / Nom'))
name = content.items[i+1].str;
if (value.includes('Date:'))
vaccinationDate = content.items[i+1].str;
if (value.includes('Product name'))
vaccineName = content.items[i+1].str;
if (value.includes('Date of birth'))
dateOfBirth = content.items[i+1].str;
if (value.includes('You have received'))
numDoses = Number(value.split(' ')[3]);
}
const receipt = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, numDoses);
return Promise.resolve(receipt);
}

4782
yarn.lock

File diff suppressed because it is too large Load Diff