-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 89aa770
Showing
9 changed files
with
1,098 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
node_modules/ | ||
lib/ | ||
|
||
*.jpg | ||
*.png | ||
*.webp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"name": "certificate_qr", | ||
"version": "1.0.0", | ||
"license": "MIT", | ||
"private": true, | ||
"exports": { | ||
"./common": "./lib/common.js", | ||
"./generator": "./lib/generator.js", | ||
"./verifier": "./lib/verifier.js", | ||
"./presenter": "./lib/presenter.js" | ||
}, | ||
"scripts": { | ||
"build": "tsc", | ||
"start": "yarn build && node -r source-map-support/register ./lib/cli.js" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^16.11.6", | ||
"@types/qrcode": "^1.4.1", | ||
"@types/sharp": "^0.29.2", | ||
"source-map-support": "^0.5.20", | ||
"typescript": "^4.4.4" | ||
}, | ||
"dependencies": { | ||
"protobufjs": "^6.11.2", | ||
"qrcode": "^1.4.4", | ||
"sharp": "^0.29.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import {promises as fs} from 'fs' | ||
import { decodeSignedCertificate, encodeCertificate, SignedVaccineCertificate, VaccinationStatus } from './common' | ||
import { buildCertificate, generateKeypair, signCertificate } from './generator' | ||
import { generateQrCode } from './presenter' | ||
import { verifyCertificate } from './verifier' | ||
|
||
(async () => { | ||
// Generate Keypair | ||
const {privateKey, publicKey} = generateKeypair() | ||
console.log(privateKey.export({ | ||
type: 'pkcs1', | ||
format: 'pem' | ||
})) | ||
console.log(publicKey.export({ | ||
type: 'pkcs1', | ||
format: 'pem' | ||
})) | ||
|
||
// Generate certificate | ||
const unsignedCert = await buildCertificate("Person ABCDEF GHIJKLMNOP", "./avatar.jpg", VaccinationStatus.FULLY_VACCINATED) | ||
|
||
const cert: SignedVaccineCertificate = signCertificate(unsignedCert, privateKey) | ||
|
||
console.log(cert) | ||
|
||
const data = encodeCertificate(cert) | ||
console.log(`Encoded to: ${data.toString("base64")}`) | ||
const decodedCert = decodeSignedCertificate(data) | ||
|
||
if (verifyCertificate(decodedCert, publicKey)) { | ||
console.log("Certificate Verified!") | ||
} | ||
else { | ||
console.log("Certificate verification failed.") | ||
} | ||
|
||
if (decodedCert.name === cert.name && decodedCert.imageData.equals(cert.imageData) && decodedCert.vaccinationStatus === cert.vaccinationStatus) { | ||
console.log("Certificates are identical.") | ||
} | ||
else { | ||
console.log("Certificates differ.") | ||
} | ||
|
||
// Write SignedCertificate out as a QR code. | ||
generateQrCode(cert, "cert.png") | ||
|
||
// Write picture out | ||
await fs.writeFile("qr_avatar.jpg", cert.imageData) | ||
})().then( | ||
() => console.log("done."), | ||
(ex) => { | ||
throw ex | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { Field, Message, Type } from "protobufjs" | ||
import zlib from "zlib" | ||
|
||
export enum VaccinationStatus { | ||
UNVACCINATED = 0, | ||
PARTIAlLY_VACCINATED, | ||
FULLY_VACCINATED | ||
} | ||
|
||
export interface IVaccineCertificate { | ||
name: string, | ||
imageData: Buffer, | ||
vaccinationStatus: VaccinationStatus, | ||
} | ||
export interface ISignedVaccineCertificate extends IVaccineCertificate { | ||
signature: Buffer | ||
} | ||
|
||
@Type.d("VaccineCertificate") | ||
export class VaccineCertificate extends Message<VaccineCertificate> implements IVaccineCertificate { | ||
@Field.d(1, "string", "required") | ||
public name: string | ||
|
||
@Field.d(2, "bytes", "required") | ||
public imageData: Buffer | ||
|
||
@Field.d(3, VaccinationStatus, "required") | ||
public vaccinationStatus: VaccinationStatus | ||
} | ||
|
||
@Type.d("SignedVaccineCertificate") | ||
export class SignedVaccineCertificate extends Message<SignedVaccineCertificate> implements ISignedVaccineCertificate { | ||
@Field.d(1, "string", "required") | ||
public name: string | ||
|
||
@Field.d(2, "bytes", "required") | ||
public imageData: Buffer | ||
|
||
@Field.d(3, VaccinationStatus, "required") | ||
public vaccinationStatus: VaccinationStatus | ||
|
||
@Field.d(4, "bytes", "required") | ||
public signature: Buffer | ||
} | ||
|
||
const _baseEncodeCertificate = (cert: VaccineCertificate | SignedVaccineCertificate): Uint8Array => { | ||
if (cert instanceof SignedVaccineCertificate) { | ||
return SignedVaccineCertificate.encode(cert).finish() | ||
} | ||
else { | ||
return VaccineCertificate.encode(cert).finish() | ||
} | ||
} | ||
|
||
export const encodeCertificate = (cert: VaccineCertificate | SignedVaccineCertificate): Buffer => { | ||
return zlib.brotliCompressSync( | ||
Buffer.from(_baseEncodeCertificate(cert)), | ||
{ | ||
params: [zlib.constants.BROTLI_PARAM_MODE, zlib.constants.BROTLI_MODE_GENERIC] | ||
} | ||
) | ||
} | ||
|
||
export const decodeSignedCertificate = (data: Buffer): SignedVaccineCertificate => { | ||
return SignedVaccineCertificate.decode(zlib.brotliDecompressSync(data)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import crypto, { KeyObject } from "crypto" | ||
import sharp from "sharp" | ||
import { encodeCertificate, SignedVaccineCertificate, VaccinationStatus, VaccineCertificate } from "./common" | ||
|
||
export const generateKeypair = () => { | ||
return crypto.generateKeyPairSync("rsa", { | ||
modulusLength: 2048 | ||
}) | ||
} | ||
|
||
interface BuildCertificateOptions { | ||
image: { | ||
width?: number, | ||
height?: number, | ||
quality?: number, | ||
greyscale?: boolean | ||
} | ||
} | ||
|
||
/** | ||
* Builds a vaccine certificate, potentially throwing a variety of errors as it goes. | ||
* @param name Name of the person being issued the certificate. | ||
* @param imagePath String path to the image to attach to the certificate (for example, a passport photo). This will be resized and heavily compressed for QR-code friendliness. | ||
* @param vaccinationStatus Value describing the person's vaccination status | ||
* @param options @see BuildCertificateOptions | ||
*/ | ||
export const buildCertificate = async (name: string, imagePath: string, vaccinationStatus: VaccinationStatus, options?: BuildCertificateOptions): Promise<VaccineCertificate> => { | ||
const { | ||
image: { | ||
width = 100, | ||
height = 100, | ||
greyscale = true, | ||
quality = 30 | ||
} | ||
} = options ?? { | ||
image: {} | ||
} | ||
|
||
let sharpImage = sharp(imagePath) | ||
.resize({ | ||
fit: 'contain', | ||
width, | ||
height | ||
}) | ||
.removeAlpha() | ||
|
||
if (greyscale) { | ||
sharpImage = sharpImage | ||
.greyscale() | ||
.toColorspace('b-w') | ||
} | ||
|
||
const imageData = await sharpImage | ||
.jpeg({quality: quality}) | ||
.toBuffer() | ||
|
||
return new VaccineCertificate({name, imageData, vaccinationStatus}) | ||
} | ||
|
||
export const signCertificate = (cert: VaccineCertificate, privateKey: KeyObject): SignedVaccineCertificate => { | ||
const signature = crypto.sign("sha256", encodeCertificate(cert), { | ||
key: privateKey, | ||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING | ||
}) | ||
|
||
return new SignedVaccineCertificate({ | ||
name: cert.name, | ||
imageData: cert.imageData, | ||
vaccinationStatus: cert.vaccinationStatus, | ||
signature | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import QRCode from "qrcode"; | ||
import { encodeCertificate, SignedVaccineCertificate } from "./common"; | ||
|
||
export const generateQrCode = async (certificate: SignedVaccineCertificate, outPath: string): Promise<void> => { | ||
return QRCode.toFile(outPath, [ | ||
// @ts-ignore | ||
{data: encodeCertificate(certificate), mode: 'byte'} | ||
]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import crypto, { KeyObject } from "crypto" | ||
import { encodeCertificate, SignedVaccineCertificate, VaccineCertificate } from "./common" | ||
|
||
export const verifyCertificate = (cert: SignedVaccineCertificate, publicKey: KeyObject): boolean => { | ||
const unsignedCert = new VaccineCertificate({ | ||
name: cert.name, | ||
imageData: cert.imageData, | ||
vaccinationStatus: cert.vaccinationStatus | ||
}) | ||
|
||
return crypto.verify("sha256", encodeCertificate(unsignedCert), { | ||
key: publicKey, | ||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING | ||
}, cert.signature) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"compilerOptions": { | ||
"isolatedModules": true, | ||
"esModuleInterop": true, | ||
"moduleResolution": "node", | ||
"target": "es2015", | ||
"module": "CommonJS", | ||
"inlineSourceMap": true, | ||
"experimentalDecorators": true, | ||
"rootDir": "src", | ||
"outDir": "lib" | ||
} | ||
} |
Oops, something went wrong.