Skip to content

Commit

Permalink
MVP API complete
Browse files Browse the repository at this point in the history
  • Loading branch information
tfinlay committed Oct 31, 2021
0 parents commit 89aa770
Show file tree
Hide file tree
Showing 9 changed files with 1,098 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
lib/

*.jpg
*.png
*.webp
28 changes: 28 additions & 0 deletions package.json
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"
}
}
54 changes: 54 additions & 0 deletions src/cli.ts
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
}
)
66 changes: 66 additions & 0 deletions src/common.ts
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))
}
72 changes: 72 additions & 0 deletions src/generator.ts
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
})
}
9 changes: 9 additions & 0 deletions src/presenter.ts
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'}
])
}
15 changes: 15 additions & 0 deletions src/verifier.ts
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)
}
13 changes: 13 additions & 0 deletions tsconfig.json
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"
}
}
Loading

0 comments on commit 89aa770

Please sign in to comment.