first commit
This commit is contained in:
68
src/lib/sign.ts
Normal file
68
src/lib/sign.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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 = any>(
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user