Skip to content

Commit

Permalink
fix(qr): probably the correct implementation (finally)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vexcited committed Nov 5, 2024
1 parent 326845b commit 4e8537b
Showing 1 changed file with 48 additions and 23 deletions.
71 changes: 48 additions & 23 deletions src/api/qr-pay.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,73 @@
import { AsnParser } from "@peculiar/asn1-schema";
import { PrivateKeyInfo } from "@peculiar/asn1-pkcs8";
import { ECPrivateKey } from "@peculiar/asn1-ecc";
import { p256 } from "@noble/curves/p256";
import { PrivateKeyInfo } from "@peculiar/asn1-pkcs8";

import { hashWithHMAC } from "~/core/hmac";
import { sha256 } from "@noble/hashes/sha256";
import { base64 } from "@scure/base";
import { bytesToHex } from "@noble/hashes/utils";

import { base64 } from "@scure/base";
import { p256 } from "@noble/curves/p256";

import { otp } from "~/api/private/otp";
import type { Identification } from "~/models";
import { otp } from "./private/otp";
import { hashWithHMAC } from "~/core/hmac";

// NOTE: We're only using `IZLY` for now but
// in the app there's also `SMONEY` as a mode.
const QrCodeMode = {
IZLY: "AIZ",
SMONEY: "A"
} as const;

const signWithPrivateKey = (textToSign: string, pem: string): Uint8Array => {
const cert = AsnParser.parse(base64.decode(pem), PrivateKeyInfo);
const keys = AsnParser.parse(cert.privateKey.buffer, ECPrivateKey);
/**
* Generates the signature for the last
* part of the QR code payload.
*/
const sign = (content: string, keyInfo: string): Uint8Array => {
const info = AsnParser.parse(base64.decode(keyInfo), PrivateKeyInfo);
const keys = AsnParser.parse(info.privateKey.buffer, ECPrivateKey);
const privateKey = new Uint8Array(keys.privateKey.buffer);

const hash = sha256.create();
hash.update(textToSign);
const hash = sha256.create().update(content).digest();
const signed = p256.sign(hash, privateKey).toDERRawBytes();

// Here's how we can debug this function...
// Prerequisites: have the same inputs as the app when generating the QR code,
// so make sure to have the same `identification` object
// and `date` string as the payload.
//
// 1. Retrieve the public key from the private key
// - Luckily, Izly provides the public key in the ECPrivateKey structure.
// const publicKey = p256.getPublicKey(privateKey);
//
// 2. Grab the last part of the QR code payload
// - so the part we're generating in this function...
// const signedFromKotlin = base64.decode("MEUCIG1jEvjmNjx8PWK7u5BwaMverup7vvzSVkI6TYoyRh22AiEA4lmlaaOhu18E3oMo6uMmVQoFzShMZU0Sy8EhaOPbgQQ=");
//
// 3. Compare the app's generated signature with the hash we generated !
// const verified = p256.verify(signedFromKotlin, hash, publicKey);
// console.log("verified:", verified);
//
// When `verified` is true, the signature is valid.
// Othrwise, the signature is invalid and there's work to do...

const signature = p256.sign(hash.digest(), privateKey);
return signature.toDERRawBytes();
return signed;
};

/**
* @returns data to show in a qrcode
* Generates the payload that are contained
* in the QR codes for the payment in the app.
*/
export const qrPay = (identification: Identification): string => {
// Replicate `SimpleDateFormat("yyyy-MM-dd HH:mm:ss")`
const dateFormatter = new Intl.DateTimeFormat("fr-CA", { timeZone: "UTC", year: "numeric", month: "2-digit", day: "2-digit", hour12: false, second: "2-digit", minute: "2-digit", hour: "2-digit" });
const date = dateFormatter.format(new Date()).replace(",", "");
const hotpCode = otp(identification);

const hotpCode = otp(identification.seed, identification.refreshCount);
identification.refreshCount++; // should increment because we called `otp()`
const substring = hotpCode.substring(0, hotpCode.length - 1);

const str = `${QrCodeMode.IZLY};${identification.userPublicID};${date};3`;
let sb = str + ";";
sb += bytesToHex(hashWithHMAC(`${str}+${identification.nsse}`, substring)) + ";";

const signed = base64.encode(signWithPrivateKey(sb, identification.qrCodePrivateKey));
const content = `${QrCodeMode.IZLY};${identification.userPublicID};${date};3`;
const hmac = bytesToHex(hashWithHMAC(`${content}+${identification.nsse}`, hotpCode));
const payload = `${content};${hmac};`;

return sb + signed;
// Concatenate payload with signature.
return payload + base64.encode(sign(payload, identification.qrCodePrivateKey));
};

0 comments on commit 4e8537b

Please sign in to comment.