69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
const te = new TextEncoder();
|
|
|
|
function b64uToBytes(input: string): Uint8Array {
|
|
// pad and replace
|
|
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
const pad = input.length % 4;
|
|
if (pad) input += "=".repeat(4 - pad);
|
|
return new Uint8Array(Buffer.from(input, "base64"));
|
|
}
|
|
|
|
function bytesToB64u(bytes: Uint8Array | ArrayBuffer): string {
|
|
const buf = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
return Buffer.from(buf)
|
|
.toString("base64")
|
|
.replace(/=/g, "")
|
|
.replace(/\+/g, "-")
|
|
.replace(/\//g, "_");
|
|
}
|
|
|
|
async function importKey(secret: string): Promise<CryptoKey> {
|
|
return await crypto.subtle.importKey(
|
|
"raw",
|
|
te.encode(secret),
|
|
{ name: "HMAC", hash: "SHA-256" },
|
|
false,
|
|
["sign", "verify"],
|
|
);
|
|
}
|
|
|
|
export async function signPayload(
|
|
payload: unknown,
|
|
secret: string,
|
|
): Promise<string> {
|
|
const payloadJson =
|
|
typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
const payloadB64u = bytesToB64u(te.encode(payloadJson));
|
|
const key = await importKey(secret);
|
|
const sig = await crypto.subtle.sign("HMAC", key, te.encode(payloadB64u));
|
|
const sigB64u = bytesToB64u(new Uint8Array(sig));
|
|
return `${payloadB64u}.${sigB64u}`;
|
|
}
|
|
|
|
export async function verifyAndDecode<T = unknown>(
|
|
token: string,
|
|
secret: string,
|
|
): Promise<{ valid: boolean; payload?: T }> {
|
|
const [payloadB64u, sigB64u] = token.split(".");
|
|
if (!payloadB64u || !sigB64u) return { valid: false };
|
|
const key = await importKey(secret);
|
|
const expected = await crypto.subtle.sign(
|
|
"HMAC",
|
|
key,
|
|
te.encode(payloadB64u),
|
|
);
|
|
const given = b64uToBytes(sigB64u);
|
|
const expectedBytes = new Uint8Array(expected);
|
|
if (given.length !== expectedBytes.length) return { valid: false };
|
|
// Constant-time compare
|
|
let diff = 0;
|
|
for (let i = 0; i < given.length; i++) diff |= given[i] ^ expectedBytes[i];
|
|
if (diff !== 0) return { valid: false };
|
|
try {
|
|
const json = Buffer.from(b64uToBytes(payloadB64u)).toString("utf8");
|
|
return { valid: true, payload: JSON.parse(json) as T };
|
|
} catch {
|
|
return { valid: false };
|
|
}
|
|
}
|