UNPKG

multipass-everywhere

Version:

Shopify Multipass authentication library with WebCrypto API. WinterTC compatible - works in all JavaScript runtimes (Node.js, Deno, Cloudflare Workers, browsers) using standard Web APIs.

69 lines (67 loc) 2.49 kB
//#region src/multipass.ts const BLOCK_SIZE = 16; var Multipass = class { _encryptionKey; _signingKey; _initialized; constructor(secret) { if (!secret) throw new Error("Invalid Secret"); this._initialized = this._initializeKeys(secret); } async _initializeKeys(secret) { const encoder = new TextEncoder(); const secretData = encoder.encode(secret); const hashBuffer = await crypto.subtle.digest("SHA-256", secretData); const hashArray = new Uint8Array(hashBuffer); const encKeyData = hashArray.slice(0, BLOCK_SIZE); const signKeyData = hashArray.slice(BLOCK_SIZE, 32); this._encryptionKey = await crypto.subtle.importKey("raw", encKeyData, { name: "AES-CBC" }, false, ["encrypt"]); this._signingKey = await crypto.subtle.importKey("raw", signKeyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); } async encode(obj) { await this._initialized; if (!obj) throw new Error("No data encoded"); obj.created_at = new Date().toISOString(); const cipherText = await this.encrypt(JSON.stringify(obj)); const signature = await this.sign(cipherText); const combined = new Uint8Array(cipherText.byteLength + signature.byteLength); combined.set(new Uint8Array(cipherText), 0); combined.set(new Uint8Array(signature), cipherText.byteLength); return this._arrayBufferToBase64Url(combined.buffer); } async generateUrl(obj, domain) { if (!domain) throw new Error("No domain specified"); const token = await this.encode(obj); return `https://${domain}/account/login/multipass/${token}`; } async sign(data) { await this._initialized; return await crypto.subtle.sign({ name: "HMAC" }, this._signingKey, data); } async encrypt(plaintext) { await this._initialized; const iv = crypto.getRandomValues(new Uint8Array(BLOCK_SIZE)); const encoder = new TextEncoder(); const data = encoder.encode(plaintext); const encryptedData = await crypto.subtle.encrypt({ name: "AES-CBC", iv }, this._encryptionKey, data); const result = new Uint8Array(iv.length + encryptedData.byteLength); result.set(iv, 0); result.set(new Uint8Array(encryptedData), iv.length); return result.buffer; } _arrayBufferToBase64Url(buffer) { const bytes = new Uint8Array(buffer); let binary = ""; for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); const base64 = btoa(binary); return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } }; //#endregion exports.Multipass = Multipass;