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:
parent
de82c494ad
commit
0d5bfd9203
@ -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')}
|
{t('index:iAcceptThe')}
|
||||||
@ -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">
|
||||||
|
@ -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
8455
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
@ -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',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user