dcc-decoder
Version:
<img alttext="COVID Green Logo" src="https://raw.githubusercontent.com/lfph/artwork/master/projects/covidgreen/stacked/color/covidgreen-stacked-color.png" width="300" />
190 lines (161 loc) • 4.86 kB
text/typescript
import pako from 'pako'
import cbor from 'cbor'
import cose from 'cose-js'
import {
ALGOS,
CBOR_STRUCTURE,
HEADER_KEYS,
PAYLOAD_KEYS,
CertificateContent,
CERT_TYPE,
} from '../types/hcert'
import { mapToJSON } from '../util'
import { SigningKeys, SigningKey, VerificationResult } from '../types'
import * as errors from '../types/errors'
import verifySignature from './verify-signature'
// eslint-disable-next-line
const base45 = require('base45-js')
function getCountry(
cert: CertificateContent,
type: CERT_TYPE,
iss?: string
): string | null {
try {
return iss || cert[type][0].co
} catch (e) {
return null
}
}
function getKid(protectedHeader, unprotectedHeader): string | null {
try {
if (protectedHeader) {
return protectedHeader.get(HEADER_KEYS.KID).toString('base64')
} else {
return unprotectedHeader.get(HEADER_KEYS.KID).toString('base64')
}
} catch {
return null
}
}
function getAlgo(protectedHeader, unprotectedHeader): ALGOS | null {
try {
if (protectedHeader) {
return protectedHeader.get(HEADER_KEYS.ALGORITHM)
} else {
return unprotectedHeader.get(HEADER_KEYS.ALGORITHM)
}
} catch {
return null
}
}
function getCertType(cert: CertificateContent): CERT_TYPE {
// Just checking if key exists does not suffice.
// It should have an item in the array too.
// Handles case ES/1.0.0/specialcases/VAC_4.png
if (cert.v?.[0]) return CERT_TYPE.VACCINE
if (cert.t?.[0]) return CERT_TYPE.TEST
if (cert.r?.[0]) return CERT_TYPE.RECOVERY
}
// function getSignature(message): string | null {
// try {
// return cbor.decodeFirstSync(message.value[CBOR_STRUCTURE.SIGNATURE])
// } catch {
// return null
// }
// }
function ensureCOSEStructure(qrCbor) {
const message = cbor.decodeFirstSync(qrCbor)
if (message instanceof cbor.Tagged && message.tag) {
return qrCbor
}
return cbor.encodeCanonical(new cbor.Tagged(cose.sign.Sign1Tag, message))
}
function decodeCbor(qrCbor): {
kid: string
country: string
issuedAt: number
expiresAt: number
rawCert: CertificateContent
algo: ALGOS
type: CERT_TYPE
} {
const message = cbor.decodeFirstSync(qrCbor)
const protectedHeader = cbor.decodeFirstSync(
message.value[CBOR_STRUCTURE.PROTECTED_HEADER]
)
const unprotectedHeader = message.value[CBOR_STRUCTURE.UNPROTECTED_HEADER]
const content = cbor.decodeFirstSync(message.value[CBOR_STRUCTURE.PAYLOAD])
// const signature = getSignature(message)
const kid = getKid(protectedHeader, unprotectedHeader)
const algo = getAlgo(protectedHeader, unprotectedHeader)
const rawCert = mapToJSON(content.get(PAYLOAD_KEYS.CONTENT).get(1))
const type = getCertType(rawCert)
const country = getCountry(rawCert, type, content.get(PAYLOAD_KEYS.ISSUER))
// move into a function as mapping/transformation grows
if (type === CERT_TYPE.TEST) {
if (rawCert.t[0].sc instanceof Date) {
rawCert.t[0].sc = rawCert.t[0].sc.toISOString()
}
}
return {
kid,
country,
issuedAt: content.get(PAYLOAD_KEYS.ISSUED_AT),
expiresAt: content.get(PAYLOAD_KEYS.EXPIRES_AT),
rawCert,
algo,
type,
}
}
function findKeysToValidateAgainst(
country: string,
kid: string,
signingKeys: SigningKey[]
): SigningKey[] {
const keys: SigningKey[] = []
if (kid) {
keys.push(...signingKeys.filter(s => s.kid === kid))
}
if (keys.length === 0 && !kid) {
keys.push(...signingKeys.filter(s => s.country === country))
}
return keys
}
export default async function decodeQR(
qr: string,
signingKeys: SigningKeys
): Promise<VerificationResult> {
if (!qr.startsWith('HC1:')) {
return { error: errors.invalidQR() }
}
try {
const qrBase45 = qr.replace('HC1:', '')
const qrZipped = base45.decode(qrBase45)
const qrCbor = ensureCOSEStructure(Buffer.from(pako.inflate(qrZipped)))
// We decode the whole cbor
const { kid, rawCert, country, expiresAt, algo, type, issuedAt } =
decodeCbor(qrCbor)
const keysToUse = findKeysToValidateAgainst(country, kid, signingKeys)
const wrapperData: Record<string, unknown> = {
country,
kid,
algo,
expiresAt: new Date(expiresAt * 1000),
issuedAt: new Date(issuedAt * 1000),
keysLength: keysToUse.length,
type,
}
if (new Date(expiresAt * 1000) < new Date()) {
return { rawCert, error: errors.certExpired(), type, wrapperData }
}
if (keysToUse.length === 0) {
return { rawCert, error: errors.noMatchingSigKey(), type, wrapperData }
}
const result = await verifySignature(qrCbor, algo, keysToUse)
const error = result instanceof Error ? result : undefined
return { rawCert, type, error, wrapperData }
} catch (err) {
console.log('Error:', err)
return { error: errors.invalidData() }
}
}