Merge pull request #40 from billylo1/shc-refactoring

Add full SHC card generation support to the app
This commit is contained in:
Billy Lo 2021-10-14 22:27:58 -04:00 committed by GitHub
commit 84c1a91da2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1399 additions and 385 deletions

View File

@ -21,8 +21,8 @@ yarn dev
### Run the Docker container
```sh
docker build . -t covidpass
docker run -t -i -p 3000:3000 covidpass
docker build . -t covidpass -t gcr.io/broadcast2patients/covidpass
docker run --rm -t -i -p 3000:3000 covidpass
```
### Integration with other repos required
@ -33,7 +33,7 @@ docker run -t -i -p 3000:3000 covidpass
#### I do not want to trust a third party with my vaccination data, does this tool respect my privacy?
Processing of your data happens entirely in your browser and only a hashed representation is sent to the server for the signing step.
Processing of your data happens entirely in your browser and only a hashed representation is sent to the server for the signing step. For more details of this, please see https://toronto.ctvnews.ca/video?clipId=2294461
#### How do I make sure that nobody can access my vaccination pass from the lock screen (iOS)?

View File

@ -263,28 +263,26 @@ function Form(): JSX.Element {
if (payloadBody) {
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];
let filenameDetails = '';
if (payloadBody.rawData.length > 0) {
// This is an SHC receipt, so do our SHC thing
selectedReceipt = payloadBody.shcReceipt;
filenameDetails = selectedReceipt.cardOrigin.replace(' ', '-');
} else {
selectedReceipt = payloadBody.receipts[selectedDose];
const vaxName = selectedReceipt.vaccineName.replace(' ', '-');
const passDose = selectedReceipt.numDoses;
filenameDetails = `${vaxName}-${passDose}`;
}
const passName = selectedReceipt.name.replace(' ', '-');
const vaxName = selectedReceipt.vaccineName.replace(' ', '-');
const passDose = selectedReceipt.numDoses;
const covidPassFilename = `grassroots-receipt-${passName}-${vaxName}-${passDose}.pkpass`;
const covidPassFilename = `grassroots-receipt-${passName}-${filenameDetails}.pkpass`;
console.log('> increment count');
await incrementCount();
console.log('> generatePass');
const pass = await PassData.generatePass(payloadBody, selectedDose);
console.log('> create blob');
const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"});
console.log(`> save blob as ${covidPassFilename}`);
await incrementCount();
saveAs(passBlob, covidPassFilename);
setSaveLoading(false);
}
@ -325,30 +323,40 @@ function Form(): JSX.Element {
try {
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));
let photoBlob: Blob;
let filenameDetails = '';
if (payloadBody.rawData.length > 0) {
// This is an SHC receipt, so do our SHC thing
selectedReceipt = payloadBody.shcReceipt;
photoBlob = await Photo.generateSHCPass(payloadBody);
filenameDetails = selectedReceipt.cardOrigin.replace(' ', '-');
} else {
// This is an old-style ON custom QR code Receipt
selectedReceipt = payloadBody.receipts[selectedDose];
const vaxName = selectedReceipt.vaccineName.replace(' ', '-');
const passDose = selectedReceipt.numDoses;
photoBlob = await Photo.generatePass(payloadBody, passDose);
filenameDetails = `${vaxName}-${passDose}`;
}
const passName = selectedReceipt.name.replace(' ', '-');
const vaxName = selectedReceipt.vaccineName.replace(' ', '-');
const passDose = selectedReceipt.numDoses;
const covidPassFilename = `grassroots-receipt-${passName}-${vaxName}-${passDose}.png`;
const covidPassFilename = `grassroots-receipt-${passName}-${filenameDetails}.png`;
await incrementCount();
let photoBlob = await Photo.generatePass(payloadBody, passDose);
saveAs(photoBlob, covidPassFilename);
// need to clean up
const qrcodeElement = document.getElementById('qrcode');
const svg = qrcodeElement.firstChild;
qrcodeElement.removeChild(svg);
const body = document.getElementById('pass-image');
body.hidden = true;
if (document.getElementById('qrcode').hasChildNodes()) {
document.getElementById('qrcode').firstChild.remove();
}
if (document.getElementById('shc-qrcode').hasChildNodes()) {
document.getElementById('shc-qrcode').firstChild.remove();
}
// Hide both our possible passes
document.getElementById('pass-image').hidden = true;
document.getElementById('shc-pass-image').hidden = true;
setSaveLoading(false);
} catch (e) {
@ -448,7 +456,7 @@ function Form(): JSX.Element {
<input type='file'
id='file'
accept="application/pdf"
accept="application/pdf,.png,.jpg,.jpeg,.gif,.webp"
ref={inputFile}
style={{display: 'none'}}
/>

View File

@ -102,7 +102,28 @@ function Page(props: PageProps): JSX.Element {
<br/>
<br/>
</div>
<canvas id="canvas" />
<div id="shc-pass-image" style={{backgroundColor: "white", color: "black", fontFamily: 'Arial', fontSize: 10, width: '350px', padding: '10px'}} hidden>
<table style={{verticalAlign: "middle"}}>
<tbody>
<tr>
<td><img src='shield-black.svg' width='50' height='50' /></td>
<td style={{fontSize: 20, width: 280}}>
<span style={{marginLeft: '11px', whiteSpace: 'nowrap'}}><b>COVID-19 Vaccination Card</b></span><br/>
<span style={{marginLeft: '11px'}}><b id='shc-card-origin'></b></span>
</td>
</tr>
</tbody>
</table>
<br/>
<br/>
<div id='shc-card-name' style={{fontSize:20, textAlign: 'center'}}></div>
<br/>
<br/>
<div id='shc-qrcode' style={{width:'63%', display:'block', marginLeft: 'auto', marginRight: 'auto'}}></div>
<br/>
<br/>
</div>
<canvas id="canvas" style={{display: 'none'}}/>
</div>
)
}

View File

@ -0,0 +1,97 @@
{
"iss": "https://smarthealthcard.phsa.ca/v1/issuer",
"nbf": 1630861482,
"vc": {
"type": [
"https://smarthealth.cards#covid19",
"https://smarthealth.cards#immunization",
"https://smarthealth.cards#health-card"
],
"credentialSubject": {
"fhirVersion": "4.0.1",
"fhirBundle": {
"resourceType": "Bundle",
"type": "collection",
"entry": [
{
"fullUrl": "resource:0",
"resource": {
"resourceType": "Patient",
"name": [
{
"family": "LASTNAME",
"given": [
"FIRSTNAME"
]
}
],
"birthDate": "YYYY-MM-DD"
}
},
{
"fullUrl": "resource:1",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
},
{
"system": "http://snomed.info/sct",
"code": "28581000087106"
}
]
},
"patient": {
"reference": "resource:0"
},
"occurrenceDateTime": "YYYY-MM-DD",
"lotNumber": "SOME_STRING",
"performer": [
{
"actor": {
"display": "Pretty Location Display Name"
}
}
]
}
},
{
"fullUrl": "resource:2",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
},
{
"system": "http://snomed.info/sct",
"code": "28581000087106"
}
]
},
"patient": {
"reference": "resource:0"
},
"occurrenceDateTime": "YYYY-MM-DD",
"lotNumber": "SOME_STRING",
"performer": [
{
"actor": {
"display": "Pretty Location Display Name"
}
}
]
}
}
]
}
}
}
}

View File

@ -0,0 +1,125 @@
{
"iss": "https://pvc.novascotia.ca/issuer",
"nbf": 1633170858.747,
"vc": {
"type": [
"https://smarthealth.cards#health-card",
"https://smarthealth.cards#immunization",
"https://smarthealth.cards#covid19"
],
"credentialSubject": {
"fhirVersion": "4.0.1",
"fhirBundle": {
"resourceType": "Bundle",
"type": "collection",
"entry": [
{
"fullUrl": "resource:0",
"resource": {
"resourceType": "Patient",
"name": [
{
"family": "LASTNAME",
"given": [
"FIRSTNAME"
]
}
],
"birthDate": "YYYY-MM-DD"
}
},
{
"fullUrl": "resource:1",
"resource": {
"resourceType": "Immunization",
"meta": {
"security": [
{
"system": "https://smarthealth.cards/ial",
"code": "IAL1.4"
}
]
},
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
},
{
"system": "http://snomed.info/sct",
"code": "28581000087106"
}
]
},
"patient": {
"reference": "resource:0"
},
"occurrenceDateTime": "YYYY-MM-DD",
"performer": [
{
"actor": {
"display": "NS, Canada"
}
}
],
"lotNumber": "SOME_STRING",
"manufacturer": {
"identifier": {
"system": "http://hl7.org/fhir/sid/mvx",
"value": "PFR"
}
}
}
},
{
"fullUrl": "resource:2",
"resource": {
"resourceType": "Immunization",
"meta": {
"security": [
{
"system": "https://smarthealth.cards/ial",
"code": "IAL1.4"
}
]
},
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
},
{
"system": "http://snomed.info/sct",
"code": "28581000087106"
}
]
},
"patient": {
"reference": "resource:0"
},
"occurrenceDateTime": "YYYY-MM-DD",
"performer": [
{
"actor": {
"display": "NS, Canada"
}
}
],
"lotNumber": "SOME_STRING",
"manufacturer": {
"identifier": {
"system": "http://hl7.org/fhir/sid/mvx",
"value": "PFR"
}
}
}
}
]
}
}
}
}

View File

@ -0,0 +1,79 @@
{
"iss": "https://covid19.quebec.ca/PreuveVaccinaleApi/issuer",
"nbf": 1630525607,
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"type": [
"VerifiableCredential",
"https://smarthealth.cards#health-card",
"https://smarthealth.cards#covid19",
"https://smarthealth.cards#immunization"
],
"credentialSubject": {
"fhirVersion": "1.0.2",
"fhirBundle": {
"resourceType": "Bundle",
"type": "Collection",
"entry": [
{
"resource": {
"resourceType": "Patient",
"name": [
{
"family": "LASTNAME",
"given": [
"First Name"
]
}
],
"birthDate": "YYYY-MM-DD"
}
},
{
"resource": {
"resourceType": "Immunization",
"status": "Completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
}
]
},
"patient": {
"reference": "resource:0"
},
"occurrenceDateTime": "YYYY-MM-DD",
"lotNumber": "SOME_STRING",
"protocolApplied": {
"doseNumber": 1,
"targetDisease": {
"coding": [
{
"system": "http://browser.ihtsdotools.org/?perspective=full&conceptId1=840536004",
"code": "840536004"
}
]
}
},
"note": [
{
"text": "PB COVID-19"
}
],
"reported": false,
"wasNotGiven": false,
"location": {
"reference": "resource:0",
"display": "LOCATION DISPLAY NAME"
}
}
}
]
}
}
}
}

View File

@ -0,0 +1,122 @@
{
"iss": "https://skphr.prd.telushealthspace.com",
"nbf": 1631923594.5185204,
"vc": {
"type": [
"VerifiableCredential",
"https://smarthealth.cards#immunization",
"https://smarthealth.cards#health-card",
"https://smarthealth.cards#covid19"
],
"credentialSubject": {
"fhirVersion": "4.0.1",
"fhirBundle": {
"resourceType": "Bundle",
"id": "00000000-0000-0000-0000-000000000000",
"type": "collection",
"entry": [
{
"fullUrl": "resource:0",
"resource": {
"resourceType": "Patient",
"name": [
{
"text": "LastName FirstName",
"family": "FirstName",
"given": [
"LastName"
]
}
],
"birthDate": "YYYY-MM-DD"
}
},
{
"fullUrl": "resource:1",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
}
]
},
"patient": {
"reference": "resource:0"
},
"meta": {
"security": [
{
"system": "https://smarthealth.cards/ial",
"code": "IAL1.4"
}
]
},
"occurrenceDateTime": "YYYY-MM-DD",
"performer": [
{
"actor": {
"display": "Government of Saskatchewan"
}
}
],
"manufacturer": {
"identifier": {
"system": "http://fhir.ehealthsask.ca/organizationIdentifiers/EHS",
"value": "Pfizer"
}
},
"lotNumber": "SOME_STRING",
"expirationDate": "YYYY-MM-DD"
}
},
{
"fullUrl": "resource:2",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
}
]
},
"patient": {
"reference": "resource:0"
},
"meta": {
"security": [
{
"system": "https://smarthealth.cards/ial",
"code": "IAL1.4"
}
]
},
"occurrenceDateTime": "YYYY-MM-DD",
"performer": [
{
"actor": {
"display": "Government of Saskatchewan"
}
}
],
"manufacturer": {
"identifier": {
"system": "http://fhir.ehealthsask.ca/organizationIdentifiers/EHS",
"value": "Pfizer"
}
},
"lotNumber": "SOME_STRING",
"expirationDate": "YYYY-MM-DD"
}
}
]
}
}
}
}

View File

@ -0,0 +1,89 @@
{
"iss": "https://myvaccinerecord.cdph.ca.gov/creds",
"nbf": 1628831358,
"vc": {
"type": [
"https://smarthealth.cards#health-card",
"https://smarthealth.cards#immunization",
"https://smarthealth.cards#covid19"
],
"credentialSubject": {
"fhirVersion": "4.0.1",
"fhirBundle": {
"resourceType": "Bundle",
"type": "collection",
"entry": [
{
"fullUrl": "resource:0",
"resource": {
"resourceType": "Patient",
"name": [
{
"family": "Lastname",
"given": [
"First Name"
]
}
],
"birthDate": "YYYY-MM-DD"
}
},
{
"fullUrl": "resource:1",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
}
]
},
"patient": {
"reference": "resource:0"
},
"occurrenceDateTime": "YYYY-MM-DD",
"lotNumber": "SOME_STRING",
"performer": [
{
"actor": {
"display": "Pretty Location Display Name"
}
}
]
}
},
{
"fullUrl": "resource:2",
"resource": {
"resourceType": "Immunization",
"status": "completed",
"vaccineCode": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/cvx",
"code": "208"
}
]
},
"patient": {
"reference": "resource:0"
},
"occurrenceDateTime": "YYYY-MM-DD",
"lotNumber": "SOME_STRING",
"performer": [
{
"actor": {
"display": "Pretty Location Display Name"
}
}
]
}
}
]
}
}
}
}

3
next-env.d.ts vendored
View File

@ -1,3 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

29
package-lock.json generated
View File

@ -11,6 +11,7 @@
"dependencies": {
"@headlessui/react": "^1.3.0",
"@ninja-labs/verify-pdf": "^0.3.9",
"@nuintun/qrcode": "^3.0.1",
"@sentry/browser": "^6.13.2",
"@sentry/integrations": "^6.13.2",
"@sentry/react": "^6.13.2",
@ -308,6 +309,19 @@
"node": ">= 8"
}
},
"node_modules/@nuintun/qrcode": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@nuintun/qrcode/-/qrcode-3.0.1.tgz",
"integrity": "sha512-0dY347xYgJvmBP0I75UHeVEx7JeDczahhEYaS03uh9bhCqa3FycdtLPxWulQh/r3OsX5lpi+z3mEXfAaQtAPwQ==",
"dependencies": {
"tslib": "^2.0.1"
}
},
"node_modules/@nuintun/qrcode/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/@sentry/browser": {
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz",
@ -5555,6 +5569,21 @@
"fastq": "^1.6.0"
}
},
"@nuintun/qrcode": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@nuintun/qrcode/-/qrcode-3.0.1.tgz",
"integrity": "sha512-0dY347xYgJvmBP0I75UHeVEx7JeDczahhEYaS03uh9bhCqa3FycdtLPxWulQh/r3OsX5lpi+z3mEXfAaQtAPwQ==",
"requires": {
"tslib": "^2.0.1"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"@sentry/browser": {
"version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz",

View File

@ -17,6 +17,7 @@
"dependencies": {
"@headlessui/react": "^1.3.0",
"@ninja-labs/verify-pdf": "^0.3.9",
"@nuintun/qrcode": "^3.0.1",
"@sentry/browser": "^6.13.2",
"@sentry/integrations": "^6.13.2",
"@sentry/react": "^6.13.2",

1
public/locales/nb-NO Symbolic link
View File

@ -0,0 +1 @@
nb

View File

@ -0,0 +1,6 @@
title: CovidPass
subtitle: Legg til EUs digitale COVID -sertifikater i favorittlommebokappene dine.
privacyPolicy: Privacy Policy
donate: Sponsor
gitHub: GitHub
imprint: Avtrykk

View File

@ -0,0 +1,20 @@
noFileOrQrCode: Vennligst skann en QR-kode, eller velg en fil
signatureFailed: Feil under signering av pass på server
decodingFailed: Kunne ikke dekode nyttelast for QR-kode
invalidColor: Ugyldig farge
certificateData: Kunne ikke lese sertifikatdata
nameMissing: Kunne ikke lese navnet
dobMissing: Kunne ikke lese fødselsdatoen
invalidMedicalProduct: Ugyldig medisinsk produkt
invalidCountryCode: Ugyldig landskode
invalidManufacturer: Ugyldig produsent
invalidFileType: Ugyldig filtype
couldNotDecode: Kunne ikke dekode QR-koden fra filen
couldNotFindQrCode: Kunne ikke finne QR-koden i den medfølgende filen
invalidQrCode: Ugyldig QR-kode
certificateType: Fant ingen gyldig sertifikattype
invalidTestResult: Ugyldig testresultat
invalidTestType: Ugyldig testtype
noCameraAccess: Fikk ikke tilgang til kameraet. Kontroller tillatelser under Innstillinger > Safari > Kamera.
noCameraFound: Fant ikke kamera.
safariSupportOnly: På iOS, vennligst bruk Safari nettleseren.

View File

@ -0,0 +1,27 @@
heading: Informasjon i henhold til § 5 TMG
contact: Kontakt
euDisputeResolution: EU Konfliktløsning
euDisputeResolutionParagraph: |
Europakommisjonen tilbyr en plattform for online konfliktløsning (OS) https://ec.europa.eu/consumers/odr.
Du finner e-postadressen vår i avtrykket ovenfor.
consumerDisputeResolution: Forbrukerkonfliktløsning
consumerDisputeResolutionParagraph: Vi er ikke villige eller forpliktet til å delta i konfliktløsningssaker for et forbrukernemnd.
liabilityForContents: Ansvar for innholdet
liabilityForContentsParagraph: |
Som tjenesteleverandør er vi ansvarlig for vårt eget innhold på disse sidene i henhold til § 7 avsnitt 1 TMG under de generelle lovene.
I henhold til §§ 8 til 10 TMG er vi ikke forpliktet til å overvåke overført eller lagret informasjon eller å undersøke omstendigheter som indikerer ulovlig aktivitet.
Forpliktelser til å fjerne eller blokkere bruk av informasjon i henhold til de generelle lovene forblir upåvirket.
Ansvar i denne forbindelse er imidlertid bare mulig fra det tidspunkt et konkret brudd på loven blir kjent.
Hvis vi blir klar over slike overtredelser, fjerner vi det relevante innholdet umiddelbart.
liabilityForLinks: Ansvar for lenker
liabilityForLinksParagraph: |
Vårt tilbud inneholder lenker til eksterne nettsteder til tredjeparter, hvis innhold vi ikke har innflytelse på.
Derfor kan vi ikke påta oss noe ansvar for dette eksterne innholdet.
Den respektive leverandøren eller operatøren av nettstedene er alltid ansvarlig for innholdet på de koblede nettstedene.
De koblede sidene ble sjekket for mulige lovbrudd på tidspunktet for koblingen.
Ulovlig innhold var ikke gjenkjennelig på tidspunktet for koblingen.
En permanent kontroll av innholdet på de lenkete sidene er imidlertid ikke rimelig uten konkrete bevis på brudd på loven.
Hvis vi blir klar over brudd, vil vi fjerne slike lenker umiddelbart.
credits: Kreditere
creditsSource: Med utdrag fra https://www.e-recht24.de/impressum-generator.html
creditsTranslation: Oversatt med https://www.DeepL.com/Translator (free version)

View File

@ -0,0 +1,29 @@
iosHint: På iOS, vennligst bruk Safari nettleseren.
errorClose: Lukk
selectCertificate: Velg Sertifikat
selectCertificateDescription: |
Skann QR-koden på sertifikatet ditt, eller velg et skjermbilde eller en PDF med QR-koden.
Vær oppmerksom på at det ikke støttes å velge en fil direkte fra kameraet.
stopCamera: Stopp Kamera
startCamera: Start Kamera
openFile: Velg Fil
foundQrCode: Fant QR-kode!
pickColor: Velg en farge
pickColorDescription: Velg en bakgrunnsfarge for passet ditt.
colorWhite: hvit
colorBlack: svart
colorGrey: grå
colorGreen: grønn
colorIndigo: mørkeblå
colorBlue: blå
colorPurple: lilla
colorTeal: blågrønn
addToWallet: Legg til i Lommebok
dataPrivacyDescription: |
Personvern er av spesiell betydning ved behandling av helserelaterte data.
For å ta en informert beslutning, vennligst les
iAcceptThe: Jeg godtar
privacyPolicy: Personvernerklæring
createdOnDevice: Laget på enheten din
openSourceTransparent: Åpen kildekode og gjennomsiktig
hostedInEU: Driftet i EU

View File

@ -0,0 +1,57 @@
gdprNotice: |
Personvernerklæringen vår er basert på vilkårene som brukes av den europeiske lovgiveren
for vedtakelsen av General Data Protection Regulation (GDPR).
generalInfo: Generell informasjon
generalInfoProcess: |
Hele prosessen med å generere passfilen skjer lokalt i nettleseren din.
For signeringstrinnet sendes bare en hash representasjon av dataene dine til serveren.
generalInfoStoring: Dine data lagres ikke utover den aktive nettlesersessionen, og nettsten bruker ikke informasjonskapsler.
generalInfoThirdParties: Ingen data blir sendt til tredjeparter.
generalInfoHttps: Vi overfører dataene dine sikkert over https.
generalInfoLocation: Serveren vår ligger i Nuremberg, Germany.
generalInfoGitHub: Kildekoden til dette nettstedet er tilgjengelig på
generalInfoLockScreen: Som standard er Apple Wallet pass tilgjengelig fra låseskjermen. Dette kan endres i
settings: innstillinger
generalInfoProvider: |
Serverleverandøren behandler data for å tilby dette nettstedet.
For å bedre forstå hvilke tiltak de tar for å beskytte dataene dine, vennligst les også deres
privacyPolicy: personvernerklæring
andThe: og
dataPrivacyFaq: vanlige spørsmål om personvern
contact: Kontakt
email: Epost
website: Webside
process: Forenklet forklaring av prosessen
processFirst: Først skjer følgende trinn lokalt i nettleseren din
processSecond: Så skjer følgende trinn på serveren vår
processThird: Til slutt skjer følgende trinn lokalt i nettleseren din
processRecognizing: Gjenkjenne og trekke ut QR-kodedataene fra det valgte sertifikatet
processDecoding: Dekoding av dine personlige og helserelaterte data fra QR-kode
processAssembling: Montering av en ufullstendig pass-fil ut av dataene dine
processGenerating: Generere en fil som inneholder hashes av dataene som er lagret i passfilen
processSending: Sender bare filen som inneholder hasjene til serveren vår
processReceiving: Motta og sjekke hashene som ble generert lokalt
processSigning: Signerer filen som inneholder hasjene
processSendingBack: Sender signaturen tilbake
processCompleting: Montering av den signerte passfilen fra den ufullstendige filen generert lokalt og signaturen
processSaving: Lagrer filen på enheten din
locallyProcessedData: Lokalt behandlede data
the:
schema: Skjema for digitalt Covid -sertifikat
specification: inneholder en detaljert spesifikasjon av hvilke data som kan finnes i QR -koden og vil bli behandlet i nettleseren din.
serverProvider: Serverleverandør
serverProviderIs: Serverleverandøren vår er
logFiles: Følgende data kan samles inn og lagres i serverloggfilene
logFilesBrowser: Nettlesertypene og versjonene som brukes
logFilesOs: Operativsystemet som brukes av tilgangssystemet
logFilesReferrer: Nettstedet som et tilgangssystem kommer til nettstedet vårt fra (såkalte henvisninger)
logFilesTime: Dato og klokkeslett for tilgang
logFilesIpAddress: De pseudonymiserte IP-adressene
rights: Dine rettigheter
rightsGranted: I samsvar med GDPR har du følgende rettigheter
rightsAccess: Rett til tilgang til dataene dine; Du har rett til å vite hvilke data som er samlet om deg og hvordan de ble behandlet.
rightsErasure: Rett til å bli glemt; Sletting av dine personlige data.
rightsRectification: Rett til å rette opp; Du har rett til å korrigere unøyaktige data.
rightsPortability: Rett til dataportabilitet; Du har rett til å overføre dataene dine fra et behandlingssystem til et annet.
thirdParties: Tredjeparter knyttet til
appleSync: Apple kan synkronisere passene dine via iCloud

1
public/locales/no Symbolic link
View File

@ -0,0 +1 @@
nb

1
public/locales/no-NO Symbolic link
View File

@ -0,0 +1 @@
no

13
public/shield-black.svg Normal file
View File

@ -0,0 +1,13 @@
<svg width="111.811" height="122.88" xmlns="http://www.w3.org/2000/svg">
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="514" width="514" y="-1" x="-1"/>
</g>
<g>
<title>Layer 1</title>
<g id="svg_1">
<path fill="#000000" id="svg_2" d="m55.713,0c20.848,13.215 39.682,19.467 55.846,17.989c2.823,57.098 -18.263,90.818 -55.63,104.891c-36.085,-13.172 -57.429,-45.441 -55.846,-105.757c18.975,0.993 37.591,-3.109 55.63,-17.123l0,0zm-21.929,66.775c-1.18,-1.01 -1.318,-2.786 -0.309,-3.967c1.011,-1.181 2.787,-1.318 3.967,-0.309l11.494,9.875l25.18,-27.684c1.047,-1.15 2.828,-1.234 3.979,-0.188c1.149,1.046 1.233,2.827 0.187,3.978l-27.02,29.708l-0.002,-0.002c-1.02,1.121 -2.751,1.236 -3.91,0.244l-13.566,-11.655l0,0zm21.951,-59.72c18.454,11.697 35.126,17.232 49.434,15.923c2.498,50.541 -16.166,80.39 -49.241,92.846c-31.942,-11.659 -50.837,-40.221 -49.435,-93.613c16.797,0.88 33.275,-2.751 49.242,-15.156l0,0z" clip-rule="evenodd" fill-rule="evenodd"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1007 B

View File

@ -1,10 +1,11 @@
// adapted from https://github.com/fproulx/shc-covid19-decoder/blob/main/src/shc.js
const jsQR = require("jsqr");
const zlib = require("zlib");
import {Receipt, HashTable} from "./payload";
import {SHCReceipt, SHCVaccinationRecord} from "./payload";
import {getVerifiedIssuer} from "./issuers";
import {registerPass, generateSHCRegisterPayload} from "./passphoto-common";
export function getQRFromImage(imageData) {
export function getQRFromImage(imageData: ImageData) {
return jsQR(
new Uint8ClampedArray(imageData.data.buffer),
imageData.width,
@ -16,92 +17,142 @@ export function getQRFromImage(imageData) {
// https://gist.github.com/alexdunae/49cc0ea95001da3360ad6896fa5677ec
// http://mchp-appserv.cpe.umanitoba.ca/viewConcept.php?printer=Y&conceptID=1514
// .vc.credentialSubject.fhirBundle.entry
export function decodedStringToReceipt(decoded: object) : HashTable<Receipt> {
const codeToVaccineName = {
'28581000087106': 'PFIZER',
'28951000087107': 'JANSSEN',
'28761000087108': 'ASTRAZENECA',
'28571000087109': 'MODERNA'
function getCvxVaccineCodeForResource(immunizationResource: any) : string {
for (let curCoding of immunizationResource.vaccineCode.coding) {
if (curCoding.system === 'http://hl7.org/fhir/sid/cvx') {
return curCoding.code;
}
}
console.error(`Cannot determine vaccine type - missing expected coding [http://hl7.org/fhir/sid/cvx] in SHC resource [${JSON.stringify(immunizationResource)}]`);
return null;
}
function getOrganizationForResource(immunizationResource: any) : string {
// By default, this is under performer actor display
if (immunizationResource.performer) {
// Assume there's only one performer (this is all that has been seen in data to date)
return immunizationResource.performer[0].actor.display;
}
// Quebec does something different from most.
if (immunizationResource.location) {
return immunizationResource.location.display;
}
console.error(`Cannot determine organization name for SHC resource [${JSON.stringify(immunizationResource)}]`);
return null;
}
export function decodedStringToReceipt(decoded: object) : SHCReceipt {
const cvxCodeToVaccineName = { // https://www2a.cdc.gov/vaccines/iis/iisstandards/vaccines.asp?rpt=cvx
'208': 'PFIZER',
'208': 'PFIZER', // Nominally for 16+, in practice seems to be used for 12+
// The two records below are for childrens' doses of Pfizer, they have different CVX codes.
'217': 'PFIZER', // 12+ dose size, unclear how this differs from 208
'218': 'PFIZER', // 5-11 dose size
'219': 'PFIZER', // 2-4 dose size
'212': 'JANSSEN',
'210': 'ASTRAZENECA',
'207': 'MODERNA'
'207': 'MODERNA',
'510': 'SINOPHARM-BIBP',
'511': 'CORONAVAC-SINOVAC',
// All of the vaccines below this line have not yet been approved by the WHO, but they are here because we have seen them all in our dataset
'502': 'COVAXIN',
'505': 'SPUTNIK V',
'506': 'CONVIDECIA',
'507': 'ZIFIVAX',
// All of the vaccines below are listed as CVX codes, but we haven't seen them yet in our data - adding for completeness
'501': 'QAZCOVID-IN',
'503': 'COVIVAC',
'500': 'UNKNOWN',
'213': 'UNKNOWN',
'509': 'EPIVACCORONA',
'508': 'CHOCELL',
'211': 'NOVAVAX',
'504': 'SPUTNIK LIGHT',
}
// Track whether the SHC code is validated - if it is not, we will record it so that we can
// proactively track new SHC codes we haven't seen yet and support them quickly if appropriate
let isValidatedSHC = true;
// 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';
const verifiedIssuer = getVerifiedIssuer(decoded['iss']);
if (!verifiedIssuer) {
// Bail out now - this is not a recognized issuer
console.error(`Issuer ${decoded['iss']} was not a recognized issuer! Cannot create card`);
isValidatedSHC = false;
}
// Now verify that this SHC deals with a COVID immunization
const cardData = decoded['vc'];
const isCovidCard = cardData.type.includes('https://smarthealth.cards#covid19');
if (!isCovidCard) {
// Bail out now - this is not a COVID card
console.error(`SHC QR code was not COVID-related (type [https://smarthealth.cards#covid19] not found)! Cannot create card`);
isValidatedSHC = false;
}
// If we're here, we have an SHC QR code which was issued by a recognized issuer. Start mapping values now
const shcResources = cardData.credentialSubject.fhirBundle.entry;
let name = '';
let dateOfBirth;
let receipts : HashTable<Receipt> = {};
let dateOfBirth = '';
const vaxRecords: SHCVaccinationRecord[] = [];
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;
}
}
switch(resource.resourceType) {
case 'Patient':
// Assume there is only one name on the card.
const nameObj = resource.name[0];
name = `${nameObj.given.join(' ')} ${nameObj.family}`;
dateOfBirth = resource.birthDate;
console.log('Detected Patient resource, added name and birthdate');
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;
break;
case 'Immunization':
// Start collecting data about this immunization record
const vaccineCode = getCvxVaccineCodeForResource(resource);
const vaccineName = cvxCodeToVaccineName[vaccineCode];
const organizationName = getOrganizationForResource(resource);
const vaccinationDate = resource.occurrenceDateTime;
const receipt = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, receiptNumber, organizationName);
// console.log(receipt);
receipts[receiptNumber] = receipt;
// Add this to our set of records
vaxRecords.push(new SHCVaccinationRecord(vaccineName, vaccinationDate, organizationName));
console.log(`Detected Immunization resource, added vaccination record (current count: ${vaxRecords.length})`);
break;
default:
console.warn(`Unexpected SHC resource type ${resource.resourceType}! Ignoring...`);
break;
}
}
return receipts;
if (name.length === 0 || dateOfBirth.length === 0) {
// Bail out now - we are missing basic info
console.error(`No name or birthdate was found! Cannot create card`);
isValidatedSHC = false;
}
const retReceipt = new SHCReceipt(name, dateOfBirth, verifiedIssuer.display, verifiedIssuer.iss, vaxRecords);
console.log(`Creating receipt for region [${retReceipt.cardOrigin}] with vaccination records [${JSON.stringify(retReceipt.vaccinations)}]`);
if (!isValidatedSHC) {
// Send this SHC to our registration endpoint so we can proactively track and react to unexpected SHCs
// (e.g. for jurisdictions we aren't aware of yet)
const registerPayload = generateSHCRegisterPayload(retReceipt);
registerPass(registerPayload);
// Now bail out
return null;
}
return retReceipt;
}

View File

@ -1,16 +1,22 @@
const issuers = [
export const issuers = [
{
id: "ca.qc",
display: "Québec",
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" },
{ kid: "2XlWk1UQMqavMtLt-aX35q_q9snFtGgdjH4-Y1gfH1M",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x: "XSxuwW_VI_s6lAw6LAlL8N7REGzQd_zXeIVDHP_j_Do",
y: "88-aI4WAEl4YmUpew40a9vq_w5OcFvsuaKMxJRLRLL0" },
]
},
{
id: "us.ca",
display: "California, USA",
iss: "https://myvaccinerecord.cdph.ca.gov/creds",
keys: [
{ kid: "7JvktUpf1_9NPwdM-70FJT3YdyTiSe2IvmVxxgDSRb0",
@ -21,6 +27,7 @@ const issuers = [
},
{
id: "us.ny",
display: "New York, USA",
iss: "https://ekeys.ny.gov/epass/doh/dvc/2021",
keys: [
{ kid: "9ENs36Gsu-GmkWIyIH9XCozU9BFhLeaXvwrT3B97Wok",
@ -31,6 +38,7 @@ const issuers = [
},
{
id: "us.la",
display: "Louisiana, USA",
iss: "https://healthcardcert.lawallet.com",
keys: [
{ kid: "UOvXbgzZj4zL-lt1uJVS_98NHQrQz48FTdqQyNEdaNE",
@ -41,16 +49,23 @@ const issuers = [
},
{
id: "ca.yt",
display: "Yukon",
iss: "https://pvc.service.yukon.ca/issuer",
keys: [
{ kid: "UnHGY-iyCIr__dzyqcxUiApMwU9lfeXnzT2i5Eo7TvE",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x5c: [
"MIICGTCCAZ6gAwIBAgIJALC8NylJvTNbMAoGCCqGSM49BAMDMDMxMTAvBgNVBAMMKEdvdmVybm1lbnQgb2YgWXVrb24gU01BUlQgSGVhbHRoIENhcmQgQ0EwHhcNMjEwODI4MjM1NzQyWhcNMjIwODI4MjM1NzQyWjA3MTUwMwYDVQQDDCxHb3Zlcm5tZW50IG9mIFl1a29uIFNNQVJUIEhlYWx0aCBDYXJkIElzc3VlcjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMAnk/a3S2E6TiudjitPqEW8A8W5YyXTYg7sT7D7f9YKIgFPi1OrHtJWJGwPMvlueeHmULUKEpScgpQtoHNjX+SjgZYwgZMwCQYDVR0TBAIwADALBgNVHQ8EBAMCB4AwOQYDVR0RBDIwMIYuaHR0cHM6Ly9zcGVjLnNtYXJ0aGVhbHRoLmNhcmRzL2V4YW1wbGVzL2lzc3VlcjAdBgNVHQ4EFgQUitSA1n/iAP1N2FYJwvvM624tgaUwHwYDVR0jBBgwFoAU5wzUU8M7Lqq4yxgxB2Yfc8neS6UwCgYIKoZIzj0EAwMDaQAwZgIxAMznKWBgcaCywPLb2/XxRaG6rnrcF5Si3JXAxi9z9PLapjjFXnn01PihQ8uf6jGM1AIxAK2ySU71gTqXbriCMq0ALOcLW0zmzcaLLEAmq5kR6iunRZNFp1v4MQxLUno5qsm2Rg==",
"MIICEzCCAXWgAwIBAgIJALQZZWjfw5NnMAoGCCqGSM49BAMEMDgxNjA0BgNVBAMMLUdvdmVybm1lbnQgb2YgWXVrb24gU01BUlQgSGVhbHRoIENhcmQgUm9vdCBDQTAeFw0yMTA4MjQyMTM0MDRaFw0yNjA4MjMyMTM0MDRaMDMxMTAvBgNVBAMMKEdvdmVybm1lbnQgb2YgWXVrb24gU01BUlQgSGVhbHRoIENhcmQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAS3TOjxd26WFX4RPaTnWcpw7/ZsfBb/+s/I7Gt+9GAsmZKjvZUDOf9Dc1ARkPh76BaE+ABzZ83LcmDqMhYdHbZXB6hOvVTmqYjtnQIovFK8irY1MsmWPfq4BW7QN6B6aKCjUDBOMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFOcM1FPDOy6quMsYMQdmH3PJ3kulMB8GA1UdIwQYMBaAFAbK320153UDRVQ30TnWECrILX8KMAoGCCqGSM49BAMEA4GLADCBhwJCAPvYBOO/NVZaglYiW35+z8UvhEadsCWfAknIdJBYkKdNQWQ0ktqS7+ctqBEVCUQng2IROSG4BnJrs7H7+1G4wgZnAkEQ+HuRcVUmiicTkPDlbwZHtoDEc1fv3TvNG9h+Qp5WvfLivz7BrFHt11ByuTCxEn7juv3B1JhpyPVZKTnQ+Nefpw==",
"MIICPTCCAaCgAwIBAgIJAOuKKbelzgyxMAoGCCqGSM49BAMEMDgxNjA0BgNVBAMMLUdvdmVybm1lbnQgb2YgWXVrb24gU01BUlQgSGVhbHRoIENhcmQgUm9vdCBDQTAeFw0yMTA4MjQyMTM0MDRaFw0zMTA4MjIyMTM0MDRaMDgxNjA0BgNVBAMMLUdvdmVybm1lbnQgb2YgWXVrb24gU01BUlQgSGVhbHRoIENhcmQgUm9vdCBDQTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAIQ94p9+W9Iszm7Nizv/NHnLKGM624IEdsJUs9MrTjCBNUsuRJNa+F99tTjTqP5u1ocbGVxSrfRxlc8Fv1fGROi5AB2c4iA+fOT55iX1TQbguCsv0YobG2YOCwHPIwcFfUm4bxTLnLjky5i7wYWPMlwj+JFmzuUkaPxFY8pdZnyMxoaSo1AwTjAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBQGyt9tNed1A0VUN9E51hAqyC1/CjAfBgNVHSMEGDAWgBQGyt9tNed1A0VUN9E51hAqyC1/CjAKBggqhkjOPQQDBAOBigAwgYYCQVSvLDHcrHXqLntgB4rXgMr7lgZ/wVBqYlzhrD8SXNNvGM8lu9KV3sWHV7n3eyyqnOR/etyb52zoKe6NBkBG+AsbAkFaxXyuwweeZHAJ379aIlKJJ2EySEOY9Bo6j+6DYd69V+lFQtNQithatUS06xLXgcXD8TF6ZcoALiP3oOwF17xHDA=="
],
x: "wCeT9rdLYTpOK52OK0-oRbwDxbljJdNiDuxPsPt_1go",
y: "IgFPi1OrHtJWJGwPMvlueeHmULUKEpScgpQtoHNjX-Q" },
]
},
{
id: "ca.bc",
display: "British Columbia",
iss: "https://smarthealthcard.phsa.ca/v1/issuer",
keys: [
{ kid: "XCqxdhhS7SWlPqihaUXovM_FjU65WeoBFGc_ppent0Q",
@ -61,6 +76,7 @@ const issuers = [
},
{
id: "ca.sk",
display: "Saskatchewan",
iss: "https://skphr.prd.telushealthspace.com",
keys: [
{ kid: "xOqUO82bEz8APn_5wohZZvSK4Ui6pqWdSAv5BEhkes0",
@ -71,6 +87,7 @@ const issuers = [
},
{
id: "ca.ab",
display: "Alberta",
iss: "https://covidrecords.alberta.ca/smarthealth/issuer",
keys: [
{ kid: "JoO-sJHpheZboXdsUK4NtfulfvpiN1GlTdNnXN3XAnM",
@ -78,9 +95,39 @@ const issuers = [
x: "GsriV0gunQpl2X9KgrDZ4EDCtIdfOmdzhdlosWrMqKk",
y: "S99mZMCcJRsn662RaAmk_elvGiUs8IvSA7qBh04kaw0" },
]
},
{
id: "ca.ns",
display: "Nova Scotia",
iss: "https://pvc.novascotia.ca/issuer",
keys: [
{ kid: "UJrT9jU8vOCUl4xsI1RZjOPP8hFUv7n9mhVtolqH9qw",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x: "kIaIeOhhxpiN13sDs6RKVzCpvxxObI9adKF5YEmKngM",
y: "AZPQ7CHd3UHp0i4a4ua1FhIq8SJ__BuHgDESuK3A_zQ" },
]
},
{
id: "ca.on",
display: "Ontario",
iss: "https://prd.pkey.dhdp.ontariohealth.ca",
keys: [
{ kid: "Nlgwb6GUrU_f0agdYKc77sXM9U8en1gBu94plufPUj8",
alg: "ES256", kty: "EC", crv: "P-256", use: "sig",
x: "ibapbMkHMlkR3D-AU0VTFDsiidQ49oD9Ha7VY8Gao3s",
y: "arXU5frZGOvTZpvg045rHC7y0fqVOS3dKqJbUYhW5gw" },
]
}
];
module.exports = {
issuers,
};
// Check for updates above at https://files.ontario.ca/apps/verify/verifyRulesetON.json
export function getVerifiedIssuer(requestIssuer) {
for (let curIssuer of issuers) {
if (curIssuer.iss === requestIssuer) {
return curIssuer;
}
}
return null;
}

View File

@ -1,11 +1,9 @@
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';
import {QrCode,Encoding,PackageResult,QrFormat,PassPhotoCommon} from './passphoto-common';
import {QrCode,PassPhotoCommon} from './passphoto-common';
import {COLORS} from "./colors";
const crypto = require('crypto')
@ -48,7 +46,7 @@ export class PassData {
const apiBaseUrl = (await configResponse.json()).apiBaseUrl
// console.log(`${apiBaseUrl}/sign`);
// console.log(JSON.stringify(signData));
console.log(JSON.stringify(signData));
const response = await fetch(`${apiBaseUrl}/sign`, {
method: 'POST',
@ -77,6 +75,10 @@ export class PassData {
const pass: PassData = new PassData(results.payload, results.qrCode);
if (!pass.expirationDate) {
delete pass['expirationDate'];
}
// Create new zip
const zip = [] as { path: string; data: Buffer | string }[];
@ -116,10 +118,12 @@ export class PassData {
// Create pass hash
const passHash = PassData.getBufferHash(Buffer.from(passJson));
const useBlackVersion = (payload.backgroundColor == COLORS.WHITE);
// Sign hash with server
const manifestSignature = await PassData.signWithRemote({
PassJsonHash: passHash,
useBlackVersion: false,
useBlackVersion: useBlackVersion,
});
// Add signature to zip
@ -139,7 +143,14 @@ export class PassData {
this.barcodes = [qrCode];
this.barcode = qrCode;
this.generic = payload.generic;
this.sharingProhibited = true;
this.expirationDate = payload.expirationDate;
// Update our pass name if this is an SHC pass
if (payload.rawData.length > 0) {
const newPassTitle = `${Constants.NAME}, ${payload.shcReceipt.cardOrigin}`;
this.logoText = newPassTitle;
this.organizationName = newPassTitle;
this.description = newPassTitle;
}
}
}

View File

@ -1,10 +1,6 @@
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';
import {Payload, PayloadBody, SHCReceipt} from "./payload";
export enum QrFormat {
PKBarcodeFormatQR = 'PKBarcodeFormatQR',
@ -28,72 +24,114 @@ export interface PackageResult {
qrCode: QrCode;
}
var _configData: any;
async function getConfigData(): Promise<any> {
if (!_configData) {
// Only call this once
const configResponse = await fetch('/api/config');
_configData = await configResponse.json();
}
return _configData;
}
export async function registerPass(registrationPayload: any) : Promise<boolean> {
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(registrationPayload) // body data type must match "Content-Type" header
}
const configData = await getConfigData();
// console.log('registering ' + JSON.stringify(registrationPayload, null, 2));
const registrationHost = configData.registrationHost;
const functionSuffix = configData.functionSuffix?? '';
const registerUrl = `${registrationHost}/register${functionSuffix}`;
// console.log(registerUrl);
try {
const response = await fetch(registerUrl, requestOptions);
const responseJson = await response.json();
// console.log(JSON.stringify(responseJson,null,2));
const wasSuccess = (responseJson["result"] === 'OK');
if (!wasSuccess) {
console.error(responseJson);
}
return Promise.resolve(wasSuccess);
} catch (e) {
console.error(e);
Sentry.captureException(e);
// Registration was unsuccessful
return false;
}
}
export function generateSHCRegisterPayload(shcReceipt: SHCReceipt) {
const retPayload = {};
retPayload['cardOrigin'] = shcReceipt.cardOrigin;
retPayload['issuer'] = shcReceipt.issuer;
for (let i = 0; i < shcReceipt.vaccinations.length; i++) {
retPayload[`vaccination_${i}`] = shcReceipt.vaccinations[i];
}
return retPayload;
}
export class PassPhotoCommon {
static async preparePayload(payloadBody: PayloadBody, numDose: number) : Promise<PackageResult> {
static async preparePayload(payloadBody: PayloadBody, numDose: number = 0) : Promise<PackageResult> {
console.log('preparePayload');
// console.log(JSON.stringify(payloadBody, null, 2), numDose);
const configData = await getConfigData();
const payload: Payload = new Payload(payloadBody, numDose);
payload.serialNumber = uuid4();
let qrCodeMessage;
let qrCodeMessage = '';
let registrationPayload: any;
if (payloadBody.rawData.startsWith('shc:/')) {
qrCodeMessage = payloadBody.rawData;
// Register an SHC pass by adding in our pertinent data fields. We only do this so we
// can detect changes from providers and react quickly - we don't need these for any
// validation since SHCs are self-validating. This entire registration process could
// be turned off for SHCs and there would be no harm to the card creation process
registrationPayload = generateSHCRegisterPayload(payloadBody.shcReceipt);
} else {
// register record
// register an old-style ON pass
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(`Error while trying to register pass!`);
}
registrationPayload = Object.assign({}, payloadBody.receipts[numDose]);
delete registrationPayload.name;
delete registrationPayload.dateOfBirth;
registrationPayload["serialNumber"] = payload.serialNumber;
registrationPayload["type"] = 'applewallet';
const verifierHost = configData.verifierHost;
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;
qrCodeMessage = `${verifierHost}/verify?${encodedUri}`;
// console.log(qrCodeUrl);
}
const wasSuccess = await registerPass(registrationPayload);
if (!wasSuccess) {
return Promise.reject(`Error while trying to register pass!`);
}
// Create QR Code Object
const qrCode: QrCode = {
message: qrCodeMessage,

View File

@ -1,17 +1,25 @@
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) {};
constructor(public name: string, public vaccinationDate: string, public vaccineName: string, public dateOfBirth: string, public numDoses: number, public organization: string) {};
}
export interface HashTable<T> {
[key: string]: T;
}
enum TextAlignment {
right = 'PKTextAlignmentRight',
center = 'PKTextAlignmentCenter'
// QR CODE NEW FORMAT:
// * Origin jurisdiction
// * Person's name
// * NOTHING ELSE ON THE CARD TO ENCOURAGE SCANNING IT TO GET DATA (this is what QC does, and what BC mostly does; other jurisdictions add more data, but that encourages bad behaviour)
export class SHCReceipt {
constructor(public name: string, public dateOfBirth: string, public cardOrigin: string, public issuer: string, public vaccinations: SHCVaccinationRecord[]) {};
}
export class SHCVaccinationRecord {
constructor(public vaccineName: string, public vaccinationDate: string, public organization: string) {};
}
interface Field {
@ -30,14 +38,15 @@ export interface PassDictionary {
}
export interface PayloadBody {
// color: COLORS;
rawData: string;
receipts: HashTable<Receipt>;
shcReceipt: SHCReceipt;
}
export class Payload {
receipts: HashTable<Receipt>;
shcReceipt: SHCReceipt;
rawData: string;
backgroundColor: string;
labelColor: string;
@ -48,44 +57,41 @@ export class Payload {
generic: PassDictionary;
expirationDate: string;
constructor(body: PayloadBody, numDose: number) {
constructor(body: PayloadBody, numDose: number = 0) {
let generic: PassDictionary = {
this.receipts = body.receipts;
this.shcReceipt = body.shcReceipt;
this.rawData = body.rawData;
this.generic = {
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;
}
}
if (body.rawData.length > 0) {
processSHCReceipt(body.shcReceipt, this.generic);
this.backgroundColor = COLORS.WHITE;
this.labelColor = COLORS.BLACK;
this.foregroundColor = COLORS.BLACK;
this.img1x = Constants.img1xBlack;
this.img2x = Constants.img2xBlack;
} else {
fullyVaccinated = processReceipt(body.receipts[numDose], generic);
const fullyVaccinated = processReceipt(body.receipts[numDose], this.generic);
if (fullyVaccinated) {
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.receipts = body.receipts;
this.rawData = body.rawData;
this.generic = generic;
if (body.rawData.length == 0) { // Ontario special handling
// These are the non-SHC ON receipts, which expire Oct 22nd
this.expirationDate = '2021-10-22T23:59:59-04:00';
generic.auxiliaryFields.push({
this.generic.auxiliaryFields.push({
key: "expiry",
label: "QR code expiry",
value: '2021-10-22'
@ -96,85 +102,100 @@ export class Payload {
function processReceipt(receipt: Receipt, generic: PassDictionary) : boolean {
console.log(`processing receipt #${receipt.numDoses}`);
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();
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('PFIZER'))
vaccineNameProper = 'Pfizer (Comirnaty)'
if (vaccineName.includes('MODERNA'))
vaccineNameProper = 'Moderna (SpikeVax)'
if (vaccineName.includes('MODERNA'))
vaccineNameProper = 'Moderna (SpikeVax)'
if (vaccineName.includes('ASTRAZENECA') || vaccineName.includes('COVISHIELD'))
vaccineNameProper = 'AstraZeneca (Vaxzevria)'
if (vaccineName.includes('ASTRAZENECA') || vaccineName.includes('COVISHIELD'))
vaccineNameProper = 'AstraZeneca (Vaxzevria)'
let doseVaccine = "#" + String(receipt.numDoses) + ": " + vaccineNameProper;
let fullyVaccinated = false;
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;
}
if (receipt.numDoses > 1 ||
vaccineName.toLowerCase().includes('janssen') ||
vaccineName.toLowerCase().includes('johnson') ||
vaccineName.toLowerCase().includes('j&j')) {
fullyVaccinated = true;
}
if (generic.primaryFields.length == 0) {
generic.primaryFields.push(
{
key: "vaccine",
label: "Vaccine",
value: doseVaccine
}
)
}
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(
if (generic.primaryFields.length == 0) {
generic.primaryFields.push(
{
key: "issuer",
label: "Authorized Organization",
value: receipt.organization
},
{
key: "dov",
label: "Vacc. Date",
value: receipt.vaccinationDate,
key: "vaccine",
label: "Vaccine",
value: doseVaccine
}
);
)
}
if (generic.auxiliaryFields.length == 0) {
generic.auxiliaryFields.push(
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: receipt.organization
},
{
key: "dov",
label: "Vacc. Date",
value: receipt.vaccinationDate,
}
);
if (generic.auxiliaryFields.length == 0) {
generic.auxiliaryFields.push(
{
key: "name",
label: "Name",
value: name
},
{
key: "dob",
label: "Date of Birth",
value: dateOfBirth
});
}
return fullyVaccinated;
}
function processSHCReceipt(receipt: SHCReceipt, generic: PassDictionary) {
console.log(`processing receipt for origin ${receipt.cardOrigin}`);
if (generic.primaryFields.length == 0) {
generic.primaryFields.push(
{
key: "name",
label: "Name",
value: name
},
{
key: "dob",
label: "Date of Birth",
value: dateOfBirth
});
}
return fullyVaccinated;
value: receipt.name
}
)
}
}

View File

@ -1,11 +1,8 @@
import {Constants} from "./constants";
import {Payload, PayloadBody} from "./payload";
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';
import {QrCode,Encoding,PackageResult,QrFormat,PassPhotoCommon} from './passphoto-common';
import { EncodeHintType } from "@zxing/library";
import {PayloadBody} from "./payload";
import { toBlob } from 'html-to-image';
import {QrCode,PassPhotoCommon} from './passphoto-common';
import { Encoder, QRByte, QRNumeric, ErrorCorrectionLevel } from '@nuintun/qrcode';
export class Photo {
@ -28,13 +25,7 @@ export class Photo {
const payload = results.payload;
const qrCode = results.qrCode;
let receipt;
if (results.payload.rawData.length == 0) {
receipt = results.payload.receipts[numDose];
} else {
receipt = results.payload.receipts[numDose];
}
let receipt = results.payload.receipts[numDose];
const body = document.getElementById('pass-image');
body.hidden = false;
@ -75,25 +66,72 @@ export class Photo {
}
}
const codeWriter = new BrowserQRCodeSvgWriter();
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 qrcode = new Encoder();
qrcode.setEncodingHint(true);
qrcode.setErrorCorrectionLevel(ErrorCorrectionLevel.L);
const blobPromise = toBlob(body);
return blobPromise;
if (qrCode.message.includes('shc:/')) {
// Write an SHC code in 2 chunks otherwise it won't render right
qrcode.write(new QRByte('shc:/'));
qrcode.write(new QRNumeric(qrCode.message.substring(5)));
} else {
// If this isn't an SHC code, just write it out as a string
qrcode.write(qrCode.message);
}
qrcode.make();
const qrImage = new Image(220, 220);
qrImage.src = qrcode.toDataURL(2, 15);
document.getElementById('qrcode').appendChild(qrImage);
return toBlob(body);
} catch (e) {
return Promise.reject(e);
}
}
private constructor(payload: Payload, qrCode: QrCode) {
static async generateSHCPass(payloadBody: PayloadBody): Promise<Blob> {
// Create Payload
try {
console.log('generateSHCPass');
const results = await PassPhotoCommon.preparePayload(payloadBody);
const qrCode = results.qrCode;
const body = document.getElementById('shc-pass-image');
body.hidden = false;
document.getElementById('shc-card-name').innerText = results.payload.shcReceipt.name;
document.getElementById('shc-card-origin').innerText = results.payload.shcReceipt.cardOrigin;
const qrcode = new Encoder();
qrcode.setEncodingHint(true);
qrcode.setErrorCorrectionLevel(ErrorCorrectionLevel.L);
if (qrCode.message.includes('shc:/')) {
// Write an SHC code in 2 chunks otherwise it won't render right
qrcode.write(new QRByte('shc:/'));
qrcode.write(new QRNumeric(qrCode.message.substring(5)));
} else {
// If this isn't an SHC code, just write it out as a string
qrcode.write(qrCode.message);
}
qrcode.make();
const qrImage = new Image(220, 220);
qrImage.src = qrcode.toDataURL(2, 15);
document.getElementById('shc-qrcode').appendChild(qrImage);
return toBlob(body);
} catch (e) {
return Promise.reject(e);
}
}
private constructor() {
// make a png in buffer using the payload
}
}

View File

@ -1,14 +1,12 @@
import {PayloadBody, Receipt, HashTable} from "./payload";
import * as PdfJS from 'pdfjs-dist/legacy/build/pdf'
import jsQR, {QRCode} from "jsqr";
import {QRCode} from "jsqr";
import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6
import {COLORS} from "./colors";
import * as Sentry from '@sentry/react';
import * as Decode from './decode';
import {getScannedJWS, verifyJWS, decodeJWS} from "./shc";
import { PNG } from 'pngjs/browser';
import { PDFPageProxy, TextContent, TextItem } from 'pdfjs-dist/types/src/display/api';
import { PDFPageProxy, TextItem } from 'pdfjs-dist/types/src/display/api';
// import {PNG} from 'pngjs'
// import {decodeData} from "./decode";
@ -19,85 +17,82 @@ PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pd
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
let imageData: ImageData[];
switch (file.type) {
case 'application/pdf':
///////// DELETE THE FOLLOWING CODE ON OCT 22ND //////////////////////
const receiptType = await detectReceiptType(fileBuffer);
console.log(`receiptType = ${receiptType}`);
// receipt type is needed to decide if digital signature checking is needed
if (receiptType == 'ON') {
receipts = await loadPDF(fileBuffer) // receipt type is needed to decide if digital signature checking is needed
// Bail out immediately, special case
const receipts = await loadPDF(fileBuffer);
return {receipts, rawData: '', shcReceipt: null};
} else {
const shcData = await processSHC(fileBuffer);
receipts = shcData.receipts;
rawData = shcData.rawData;
///////// END OCT 22ND DELETE BLOCK //////////////////////
imageData = await getImageDataFromPdf(fileBuffer);
}
break;
=======
import {PayloadBody} from "./payload";
import * as PdfJS from 'pdfjs-dist'
import jsQR, {QRCode} from "jsqr";
import {decodeData} from "./decode";
import {Result} from "@zxing/library";
import {COLORS} from "./colors";
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> {
let imageData: ImageData;
switch (file.type) {
case 'application/pdf':
console.log('pdf')
// Read file
const fileBuffer = await file.arrayBuffer();
imageData = await getImageDataFromPdf(fileBuffer)
break
case 'image/png':
case 'image/jpeg':
case 'image/webp':
case 'image/gif':
console.log(`image ${file.type}`);
imageData = [await getImageDataFromImage(file)];
break;
default:
throw Error('invalidFileType')
}
return {
receipts: receipts,
rawData: rawData
}
// Send back our SHC payload now
return processSHC(imageData);
}
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 docMetadata = await pdfDocument.getMetadata();
const numItems = content.items.length;
if (numItems == 0) { // QC has no text items
console.log('detected QC');
Sentry.setContext("QC-pdf-metadata", {
numPages: pdfDocument.numPages,
docMetadata: JSON.stringify(docMetadata)
});
Sentry.captureMessage('QC PDF Metadata for structure analysis');
console.log(`PDF details: numPages=${pdfDocument.numPages}`);
return Promise.resolve('SHC');
// Explicitly try to detect an ON PDF based on the headers in the PDF
//console.log(`PDF details: metadata=${JSON.stringify(docMetadata)}`);
// The Ontario proof-of-vaccination receipts have several fixed unchanging pieces of metadata that we use for detection
if (docMetadata.info['IsSignaturesPresent'] &&
(docMetadata.info['Producer'] == 'PDFKit') &&
(docMetadata.info['PDFFormatVersion'] == '1.7') &&
(docMetadata.info['Title'] == 'COVID-19 vaccination receipt / Récépissé de vaccination contre la COVID-19')
) {
return Promise.resolve('ON');
} 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');
Sentry.setContext("BC-pdf-metadata", {
numPages: pdfDocument.numPages,
docMetadata: JSON.stringify(docMetadata)
});
Sentry.captureMessage('BC PDF Metadata for structure analysis');
return Promise.resolve('SHC');
} else if (value.includes('Proof of Vaccination') || value.includes('User Information')) {
// possible missed QC PDF?
Sentry.setContext("Possible-missed-QC-pdf-metadata", {
numPages: pdfDocument.numPages,
docMetadata: JSON.stringify(docMetadata),
matchedValue: value
});
Sentry.captureMessage('Possible missed QC PDF Metadata for structure analysis');
}
}
// If it's not an exact match to an ON proof-of-vaccination PDF, it will never pass validation anyways so treat it as an SHC PDF
return Promise.resolve('SHC');
}
return Promise.resolve('ON');
}
async function loadPDF(fileBuffer : ArrayBuffer): Promise<HashTable<Receipt>> {
@ -150,22 +145,21 @@ async function loadPDF(fileBuffer : ArrayBuffer): Promise<HashTable<Receipt>> {
'-----END CERTIFICATE-----';
const pdfCert = result.pemCertificate.trim();
const pdfOrg = result.issuedBy.organizationName;
//const pdfOrg = result.issuedBy.organizationName;
const issuedpemCertificate = (pdfCert == refcert.trim());
//console.log(`pdf is signed by this cert ${result.pemCertificate.trim()}`);
//console.log(issuedpemCertificate);
//console.log(`PDF is signed by ${result.issuedBy.organizationName}, issued to ${result.issuedTo.commonName}`);
// const bypass = window.location.href.includes('grassroots2');
if (( issuedpemCertificate )) {
if (issuedpemCertificate) {
//console.log('getting receipt details inside PDF');
const receipt = await getPdfDetails(fileBuffer);
// console.log(JSON.stringify(receipt, null, 2));
return Promise.resolve(receipt);
} else {
/* We don't need to track these anymore - this is all going away in a week anyways
// According to the Sentry docs, this can be up to 8KB in size
// https://develop.sentry.dev/sdk/data-handling/#variable-size
Sentry.setContext("certificate", {
@ -173,6 +167,7 @@ async function loadPDF(fileBuffer : ArrayBuffer): Promise<HashTable<Receipt>> {
pdfOrg: pdfOrg,
});
Sentry.captureMessage('Certificate validation failed');
*/
console.error('invalid certificate');
return Promise.reject(`invalid certificate + ${JSON.stringify(result)}`);
}
@ -203,10 +198,9 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt
const pdfDocument = await loadingTask.promise;
// Load all dose numbers
const { numPages } = pdfDocument;
const receiptObj = {};
for (let pages = 1; pages <= numPages; pages++){
for (let pages = 1; pages <= pdfDocument.numPages; pages++){
const pdfPage = await pdfDocument.getPage(pages);
const content = await pdfPage.getTextContent();
const numItems = content.items.length;
@ -244,17 +238,17 @@ async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt
}
}
async function getImageDataFromPdf(pdfPage: PDFPageProxy): Promise<ImageData> {
async function getImageDataFromPdfPage(pdfPage: PDFPageProxy): Promise<ImageData> {
const pdfScale = 2;
const canvas = <HTMLCanvasElement>document.getElementById('canvas');
const canvasContext = canvas.getContext('2d');
const viewport = pdfPage.getViewport({scale: pdfScale})
const viewport = pdfPage.getViewport({scale: pdfScale});
// Set correct canvas width / height
canvas.width = viewport.width
canvas.height = viewport.height
canvas.width = viewport.width;
canvas.height = viewport.height;
// render PDF
const renderTask = pdfPage.render({
@ -265,54 +259,122 @@ async function getImageDataFromPdf(pdfPage: PDFPageProxy): Promise<ImageData> {
await renderTask.promise;
// Return PDF Image Data
return canvasContext.getImageData(0, 0, canvas.width, canvas.height)
return canvasContext.getImageData(0, 0, canvas.width, canvas.height);
}
async function processSHC(fileBuffer : ArrayBuffer) : Promise<any> {
function getImageDataFromImage(file: File): Promise<ImageData> {
return new Promise((resolve, reject) => {
const canvas = <HTMLCanvasElement>document.getElementById('canvas');
const canvasContext = canvas.getContext('2d');
// create Image object
const img = new Image();
img.onload = () => {
// constrain image to 2 Mpx
const maxPx = 2000000;
let width: number;
let height: number;
if (img.naturalWidth * img.naturalHeight > maxPx) {
const ratio = img.naturalHeight / img.naturalWidth;
width = Math.sqrt(maxPx / ratio);
height = Math.floor(width * ratio);
width = Math.floor(width);
} else {
width = img.naturalWidth;
height = img.naturalHeight;
}
// Set correct canvas width / height
canvas.width = width;
canvas.height = height;
// draw image into canvas
canvasContext.clearRect(0, 0, width, height);
canvasContext.drawImage(img, 0, 0, width, height);
// Obtain image data
resolve(canvasContext.getImageData(0, 0, width, height));
};
img.onerror = (e) => {
reject(e);
};
// start loading image from file
img.src = URL.createObjectURL(file);
});
}
async function getImageDataFromPdf(fileBuffer: ArrayBuffer): Promise<ImageData[]> {
const typedArray = new Uint8Array(fileBuffer);
const loadingTask = PdfJS.getDocument(typedArray);
const pdfDocument = await loadingTask.promise;
console.log('SHC PDF loaded');
const retArray = [];
// Load and return every page in our PDF
for (let i = 1; i <= pdfDocument.numPages; i++) {
console.log(`Processing PDF page ${i}`);
const pdfPage = await pdfDocument.getPage(i);
const imageData = await getImageDataFromPdfPage(pdfPage);
retArray.push(imageData);
}
return Promise.resolve(retArray);
}
async function processSHC(allImageData : ImageData[]) : Promise<PayloadBody> {
console.log('processSHC');
try {
const typedArray = new Uint8Array(fileBuffer);
const loadingTask = PdfJS.getDocument(typedArray);
if (allImageData) {
for (let i = 0; i < allImageData.length; i++) {
const pdfDocument = await loadingTask.promise;
console.log('SHC PDF loaded');
// Load all dose numbers
const pdfPage = await pdfDocument.getPage(1);
console.log('SHC PDF Page 1 loaded');
const imageData = await getImageDataFromPdf(pdfPage);
console.log('SHC PDF Page 1 image data loaded');
const code : QRCode = await Decode.getQRFromImage(imageData);
console.log('SHC code detected:');
console.log(code);
const imageData = allImageData[i];
const code : QRCode = await Decode.getQRFromImage(imageData);
//console.log(`SHC code result from page ${i}:`);
//console.log(code);
if (!code) {
return Promise.reject(new Error('No valid ON proof-of-vaccination digital signature found! Please make sure you download the PDF directly from covid19.ontariohealth.ca, Save as Files on your iPhone, and do NOT save/print it as a PDF!'));
}
if (code) {
try {
// We found a QR code of some kind - start analyzing now
const rawData = code.data;
const jws = getScannedJWS(rawData);
const decoded = await decodeJWS(jws);
const rawData = code.data;
const jws = getScannedJWS(rawData);
//console.log(decoded);
const decoded = await decodeJWS(jws);
// console.log(decoded);
const verified = verifyJWS(jws, decoded.iss);
const verified = verifyJWS(jws, decoded.iss);
if (verified) {
const receipts = Decode.decodedStringToReceipt(decoded);
//console.log(receipts);
return Promise.resolve({receipts: receipts, rawData: rawData});
} else {
return Promise.reject(`Issuer ${decoded.iss} cannot be verified.`);
if (verified) {
const shcReceipt = Decode.decodedStringToReceipt(decoded);
//console.log(shcReceipt);
return Promise.resolve({receipts: null, shcReceipt, rawData});
} else {
// If we got here, we found an SHC which was not verifiable. Consider it fatal and stop processing.
return Promise.reject(`Issuer ${decoded.iss} cannot be verified.`);
}
} catch (e) {
// We blew up during processing - log it and move on to the next page
console.log(e);
}
}
}
}
// If we got here, no SHC was detected and successfully decoded.
// The vast majority of our processed things right now are ON proof-of-vaccination PDFs, not SHC docs, so assume anything
// that blew up here was a malformed ON proof-of-vaccination and create an appropriate error message for that
return Promise.reject(new Error('No valid ON proof-of-vaccination digital signature found! Please make sure you download the PDF directly from covid19.ontariohealth.ca, Save as Files on your iPhone, and do NOT save/print it as a PDF!'));
} catch (e) {
Sentry.captureException(e);
return Promise.reject(e);
}
}
}

View File

@ -28,7 +28,7 @@ function getScannedJWS(shcString) {
function verifyJWS(jws, iss) {
const issuer = issuers.find(el => el.iss === iss);
if (!issuer) {
error = new Error("Unknown issuer " + iss);
error = new Error(`Unknown issuer ${iss}`);
error.customMessage = true;
return Promise.reject(error);
}

View File

@ -145,6 +145,13 @@
"@nodelib/fs.scandir" "2.1.5"
"fastq" "^1.6.0"
"@nuintun/qrcode@^3.0.1":
"integrity" "sha512-0dY347xYgJvmBP0I75UHeVEx7JeDczahhEYaS03uh9bhCqa3FycdtLPxWulQh/r3OsX5lpi+z3mEXfAaQtAPwQ=="
"resolved" "https://registry.npmjs.org/@nuintun/qrcode/-/qrcode-3.0.1.tgz"
"version" "3.0.1"
dependencies:
"tslib" "^2.0.1"
"@sentry/browser@^6.13.2", "@sentry/browser@6.13.2":
"integrity" "sha512-bkFXK4vAp2UX/4rQY0pj2Iky55Gnwr79CtveoeeMshoLy5iDgZ8gvnLNAz7om4B9OQk1u7NzLEa4IXAmHTUyag=="
"resolved" "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz"
@ -1473,6 +1480,11 @@
"resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
"version" "1.0.0"
"fsevents@~2.3.1", "fsevents@~2.3.2":
"integrity" "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="
"resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz"
"version" "2.3.2"
"function-bind@^1.1.1":
"integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
@ -3315,6 +3327,11 @@
"resolved" "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
"version" "1.14.1"
"tslib@^2.0.1":
"integrity" "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
"resolved" "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz"
"version" "2.3.1"
"tty-browserify@0.0.0":
"integrity" "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
"resolved" "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz"