src/lib/session/crypto.ts
/**
* @private Sign a value using a secret.
* @param {string} value - The value to sign.
* @param {string} secret - The secret to use to sign the value.
* @returns A promise that resolves to the signed value.
*/
export type SignFunction = (value: string, secret: string) => Promise<string>;
/**
* @private Unsign a value using a secret.
* @param {string} cookie - The value to unsign.
* @param {string} secret - The secret to use to unsign the value.
* @returns A promise that resolves to the unsigned value or `false` if the
*/
export type UnsignFunction = (cookie: string, secret: string) => Promise<string | false>;
const encoder = new TextEncoder();
function byteStringToUint8Array(byteString: string): Uint8Array {
const array = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
array[i] = byteString.charCodeAt(i);
}
return array;
}
async function createKey(secret: string, usages: CryptoKey['usages']): Promise<CryptoKey> {
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
usages
);
return key;
}
export const sign: SignFunction = async (value, secret) => {
const key = await createKey(secret, ['sign']);
const data = encoder.encode(value);
const signature = await crypto.subtle.sign('HMAC', key, data);
const hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(/=+$/, '');
return value + '.' + hash;
};
export const unsign: UnsignFunction = async (signed, secret) => {
const index = signed.lastIndexOf('.');
const value = signed.slice(0, index);
const hash = signed.slice(index + 1);
const key = await createKey(secret, ['verify']);
const data = encoder.encode(value);
const signature = byteStringToUint8Array(atob(hash));
const valid = await crypto.subtle.verify('HMAC', key, signature, data);
return valid ? value : false;
};