willin/svelte-session

View on GitHub
src/lib/session/crypto.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
/**
 * @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;
};