/ src / api / qr-pay.ts
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  };