qr-pay.ts
1 import type { Identification } from "~/models"; 2 import { p256 } from "@noble/curves/nist.js"; 3 4 import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js"; 5 import { ECPrivateKey } from "@peculiar/asn1-ecc"; 6 import { PrivateKeyInfo } from "@peculiar/asn1-pkcs8"; 7 8 import { AsnParser } from "@peculiar/asn1-schema"; 9 import { base64 } from "@scure/base"; 10 11 import { otp } from "~/api/private/otp"; 12 import { hashWithHMAC } from "~/core/hmac"; 13 14 // NOTE: We're only using `IZLY` for now but 15 // in the app there's also `SMONEY` as a mode. 16 const QrCodeMode = { 17 IZLY: "AIZ", 18 SMONEY: "A" 19 } as const; 20 21 /** 22 * Generates the signature for the last 23 * part of the QR code payload. 24 */ 25 const sign = (content: Uint8Array, keyInfo: string): Uint8Array => { 26 const info = AsnParser.parse(base64.decode(keyInfo), PrivateKeyInfo); 27 const keys = AsnParser.parse(info.privateKey.buffer, ECPrivateKey); 28 const privateKey = new Uint8Array(keys.privateKey.buffer); 29 30 const signed = p256.sign(content, privateKey, { format: "der" }); 31 32 // Here's how we can debug this function... 33 // Prerequisites: have the same inputs as the app when generating the QR code, 34 // so make sure to have the same `identification` object 35 // and `date` string as the payload. 36 // 37 // 1. Retrieve the public key from the private key 38 // - Luckily, Izly provides the public key in the ECPrivateKey structure. 39 // const publicKey = p256.getPublicKey(privateKey); 40 // 41 // 2. Grab the last part of the QR code payload 42 // - so the part we're generating in this function... 43 // const signedFromKotlin = base64.decode("MEQCIDQDJaRdEkAmtoucOSiKVNazGfUOHHlsmOq8ZQWdXrbWAiA2nYsGAqKpFYlINKaT3YTUsnZfKhJslzh8yrZ/5QvzOg=="); 44 // 45 // 3. Compare the app's generated signature with the hash we generated ! 46 // const verified = p256.verify(signedFromKotlin, content, publicKey, { format: "der" }); 47 // console.log("verified:", verified); 48 // 49 // When `verified` is true, the signature is valid. 50 // Othrwise, the signature is invalid and there's work to do... 51 52 return signed; 53 }; 54 55 /** 56 * Generates the payload that are contained 57 * in the QR codes for the payment in the app. 58 */ 59 export const qrPay = (identification: Identification): string => { 60 // Replicate `SimpleDateFormat("yyyy-MM-dd HH:mm:ss")` 61 const dateFormatter = new Intl.DateTimeFormat("en-CA", { day: "2-digit", hour: "2-digit", hour12: false, minute: "2-digit", month: "2-digit", second: "2-digit", timeZone: "UTC", year: "numeric" }); 62 const date = dateFormatter.format(new Date()).replace(",", ""); 63 const hotpCode = otp(identification); 64 65 const content = `${QrCodeMode.IZLY};${identification.userPublicID};${date};3`; 66 const hmac = bytesToHex(hashWithHMAC(utf8ToBytes(`${content}+${identification.nsse}`), utf8ToBytes(hotpCode))); 67 const payload = `${content};${hmac};`; 68 69 // Concatenate payload with signature. 70 return payload + base64.encode(sign(utf8ToBytes(payload), identification.qrCodePrivateKey)); 71 };