1
0
mirror of https://github.com/covidpass-org/covidpass.git synced 2025-02-23 23:17:37 +01:00

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"> <div className="space-y-5">
<p>{t('index:selectCertificateDescription')}</p> <p>{t('index:selectCertificateDescription')}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<button {/* <button
type="button" type="button"
onClick={isCameraOpen ? hideCameraView : showCameraView} onClick={isCameraOpen ? hideCameraView : showCameraView}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md"> 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')} {isCameraOpen ? t('index:stopCamera') : t('index:startCamera')}
</button> </button> */}
<button <button
type="button" type="button"
onClick={showFileDialog} onClick={showFileDialog}
@ -209,15 +209,15 @@ function Form(): JSX.Element {
} }
</div> </div>
}/> }/>
<Card step="2" heading={t('index:pickColor')} content={ {/* <Card step="2" heading={t('index:pickColor')} content={
<div className="space-y-5"> <div className="space-y-5">
<p>{t('index:pickColorDescription')}</p> <p>{t('index:pickColorDescription')}</p>
<div className="relative inline-block w-full"> <div className="relative inline-block w-full">
<Colors onChange={setSelectedColor} initialValue={selectedColor}/> <Colors onChange={setSelectedColor} initialValue={selectedColor}/>
</div> </div>
</div> </div>
}/> }/> */}
<Card step="3" heading={t('index:addToWallet')} content={ <Card step="2" heading={t('index:addToWallet')} content={
<div className="space-y-5"> <div className="space-y-5">
<p> <p>
{t('index:dataPrivacyDescription')} {t('index:dataPrivacyDescription')}
@ -231,10 +231,10 @@ function Form(): JSX.Element {
<ul className="list-none"> <ul className="list-none">
<Check text={t('createdOnDevice')}/> <Check text={t('createdOnDevice')}/>
<Check text={t('openSourceTransparent')}/> <Check text={t('openSourceTransparent')}/>
<Check text={t('hostedInEU')}/> {/* <Check text={t('hostedInEU')}/> */}
</ul> </ul>
</div> </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"/> <input type="checkbox" id="privacy" value="privacy" required className="h-5 w-5 outline-none"/>
<p> <p>
{t('index:iAcceptThe')}&nbsp; {t('index:iAcceptThe')}&nbsp;
@ -244,7 +244,7 @@ function Form(): JSX.Element {
</a> </a>
</Link>. </Link>.
</p> </p>
</label> </label> */}
<div className="flex flex-row items-center justify-start"> <div className="flex flex-row items-center justify-start">
<button id="download" type="submit" <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"> 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> <footer>
<nav className="nav flex pt-4 flex-row space-x-4 justify-center text-md font-bold flex-wrap"> <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://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> <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="/privacy"><a className="hover:underline">{t('common:privacyPolicy')}</a></Link>
<Link href="/imprint"><a className="hover:underline">{t('common:imprint')}</a></Link> <Link href="/imprint"><a className="hover:underline">{t('common:imprint')}</a></Link> */}
</nav> </nav>
</footer> </footer>
</main> </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", "build": "next build",
"start": "next start" "start": "next start"
}, },
"browser": {
"fs": false,
"os": false,
"path": false
},
"dependencies": { "dependencies": {
"@headlessui/react": "^1.3.0", "@headlessui/react": "^1.3.0",
"@ninja-labs/verify-pdf": "^0.3.9",
"@zxing/browser": "^0.0.9", "@zxing/browser": "^0.0.9",
"@zxing/library": "^0.18.6", "@zxing/library": "^0.18.6",
"base45": "^3.0.0", "base45": "^3.0.0",
@ -18,7 +24,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jpeg-js": "^0.4.3", "jpeg-js": "^0.4.3",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"next": "latest", "next": "^11.0.1",
"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",
@ -26,6 +32,7 @@
"pngjs": "^6.0.0", "pngjs": "^6.0.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"tls": "^0.0.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webpack": "^5.0.0", "webpack": "^5.0.0",
"worker-loader": "^3.0.7" "worker-loader": "^3.0.7"

View File

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

View File

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

View File

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

View File

@ -5,9 +5,15 @@ import jsQR, {QRCode} from "jsqr";
import {decodeData} from "./decode"; import {decodeData} from "./decode";
import {Result} from "@zxing/library"; import {Result} from "@zxing/library";
import {COLORS} from "./colors"; 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` 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> { export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise<PayloadBody> {
// Read file // Read file
const fileBuffer = await file.arrayBuffer(); const fileBuffer = await file.arrayBuffer();
@ -17,12 +23,12 @@ export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise
switch (file.type) { switch (file.type) {
case 'application/pdf': case 'application/pdf':
console.log('pdf') console.log('pdf')
imageData = await getImageDataFromPdf(fileBuffer) await loadPDF(fileBuffer)
break
case 'image/png':
console.log('png')
imageData = await getImageDataFromPng(fileBuffer)
break break
// case 'image/png':
// console.log('png')
// imageData = await getImageDataFromPng(fileBuffer)
// break
default: default:
throw Error('invalidFileType') throw Error('invalidFileType')
} }
@ -127,3 +133,73 @@ async function getImageDataFromPdf(fileBuffer: ArrayBuffer): Promise<ImageData>
// Return PDF Image Data // Return PDF Image Data
return canvasContext.getImageData(0, 0, canvas.width, canvas.height) 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