86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
import { SignedData, VoteData } from "./types";
|
|
/**
|
|
* Gets the WebCrypto API regardless of environment (Node vs Browser)
|
|
*/
|
|
const getCrypto = () => {
|
|
return (globalThis as any).crypto;
|
|
};
|
|
|
|
export const verifyVote = async (data: any, signatureStr: string, publicKey: CryptoKey) => {
|
|
const encoder = new TextEncoder();
|
|
const encodedData = encoder.encode(JSON.stringify(data));
|
|
|
|
// Convert Base64 back to Uint8Array
|
|
const signature = Uint8Array.from(atob(signatureStr), c => c.charCodeAt(0));
|
|
|
|
return await getCrypto().subtle.verify(
|
|
"RSASSA-PKCS1-v1_5",
|
|
publicKey,
|
|
signature,
|
|
encodedData
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Verifies a specific vote within an array of votes by
|
|
* reconstructing the "signed state" at that point in time.
|
|
*/
|
|
export const verifyChainedVote = async (
|
|
voteData: SignedData<VoteData>[],
|
|
index: number,
|
|
pubKey: CryptoKey
|
|
) => {
|
|
const voteToVerify = voteData[index];
|
|
console.log("Verifying vote: " + voteToVerify)
|
|
if(voteToVerify) {
|
|
// 1. Reconstruct the exact data state the user signed
|
|
// We need the array exactly as it was when they pushed their vote
|
|
const historicalState = voteData.slice(0, index + 1).map((v, i) => {
|
|
if (i === index) {
|
|
// For the current vote, the signature must be empty string
|
|
// because it wasn't signed yet when passed to signVote
|
|
return { ...v, signature: "" };
|
|
}
|
|
return v;
|
|
});
|
|
|
|
try {
|
|
// 3. Verify: Does this historicalState match the signature?
|
|
return await verifyVote(historicalState, voteToVerify.signature, pubKey);
|
|
} catch (err) {
|
|
console.error("Verification failed")
|
|
console.error(err);
|
|
return false;
|
|
}
|
|
}
|
|
console.error("Vote is undefined or null");
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Converts a Base64 string back into a usable CryptoKey object
|
|
* @param keyStr The Base64 string (without PEM headers)
|
|
* @param type 'public' or 'private'
|
|
*/
|
|
export const stringToCryptoKey = async (keyStr: string, type: 'public' | 'private'): Promise<CryptoKey> => {
|
|
// 1. Convert Base64 string to a Uint8Array (binary)
|
|
const bytes = Buffer.from(keyStr, 'base64');
|
|
|
|
// 2. Identify the format based on the key type
|
|
// Public keys usually use 'spki', Private keys use 'pkcs8'
|
|
const format = type === 'public' ? 'spki' : 'pkcs8';
|
|
const usages: KeyUsage[] = type === 'public' ? ['verify'] : ['sign'];
|
|
|
|
// 3. Import the key
|
|
return await getCrypto().subtle.importKey(
|
|
format,
|
|
bytes,
|
|
{
|
|
name: "RSASSA-PKCS1-v1_5",
|
|
hash: "SHA-256",
|
|
},
|
|
true, // extractable (set to false if you want to lock it in memory)
|
|
usages
|
|
);
|
|
}; |