mirror of
https://github.com/covidpass-org/covidpass.git
synced 2025-02-23 06:57:40 +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:
parent
de82c494ad
commit
0d5bfd9203
@ -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')}
|
||||
@ -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">
|
||||
|
@ -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
8455
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
@ -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',
|
||||
}}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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')
|
||||
}
|
||||
@ -127,3 +133,73 @@ 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);
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user