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
JavaScript
//#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;