UNPKG

zerokey

Version:

Zero-knowledge cross-domain secret sharing library using ECDH encryption

165 lines (162 loc) 4.8 kB
'use strict'; // src/crypto.ts function base64urlEncode(buffer) { const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer))); return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } function base64urlDecode(str) { const base64 = str.replace(/-/g, "+").replace(/_/g, "/").padEnd(str.length + (4 - str.length % 4) % 4, "="); const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; } async function generateKeyPair() { const keyPair = await crypto.subtle.generateKey( { name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey"] ); const publicKeyExported = await exportPublicKey(keyPair.publicKey); return { publicKey: publicKeyExported, privateKey: keyPair.privateKey }; } async function exportPublicKey(publicKey) { const exported = await crypto.subtle.exportKey("raw", publicKey); return base64urlEncode(exported); } async function importPublicKey(publicKeyString) { const keyData = base64urlDecode(publicKeyString); return await crypto.subtle.importKey( "raw", keyData, { name: "ECDH", namedCurve: "P-256" }, false, [] ); } async function deriveSharedSecret(privateKey, publicKey) { return await crypto.subtle.deriveKey( { name: "ECDH", public: publicKey }, privateKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); } async function aesEncrypt(key, data) { const encoder = new TextEncoder(); const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, key, encoder.encode(data) ); const combined = new Uint8Array(iv.length + encrypted.byteLength); combined.set(iv, 0); combined.set(new Uint8Array(encrypted), iv.length); return combined.buffer; } async function encryptWithPublicKey(secret, publicKeyString) { const ephemeralKeyPair = await generateKeyPair(); const recipientPublicKey = await importPublicKey(publicKeyString); const sharedKey = await deriveSharedSecret(ephemeralKeyPair.privateKey, recipientPublicKey); const encryptedData = await aesEncrypt(sharedKey, secret); const payload = { ephemeralPublicKey: ephemeralKeyPair.publicKey, encryptedData: base64urlEncode(encryptedData) }; return base64urlEncode(new TextEncoder().encode(JSON.stringify(payload))); } // src/server.ts var pendingSecret = null; var isInitialized = false; function parseQueryParams() { const params = new URLSearchParams(window.location.search); return { publicKey: params.get("publicKey"), redirect: params.get("redirect"), state: params.get("state") }; } function isValidRedirect(url) { try { const parsedUrl = new URL(url); return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"; } catch { return false; } } async function performRedirect(secret, publicKey, redirectUrl, state) { try { const encryptedSecret = await encryptWithPublicKey(secret, publicKey); const url = new URL(redirectUrl); const fragment = new URLSearchParams({ secret: encryptedSecret, state }).toString(); window.location.href = `${url.origin}${url.pathname}${url.search}#${fragment}`; } catch (error) { console.error("Failed to encrypt and redirect:", error); const url = new URL(redirectUrl); window.location.href = `${url.origin}${url.pathname}${url.search}#error=encryption_failed&state=${state}`; } } function initSecretServer(options = {}) { if (isInitialized) { console.warn("Secret server already initialized"); return; } isInitialized = true; const { publicKey, redirect, state } = parseQueryParams(); if (!publicKey || !redirect || !state) { console.error("Missing required parameters"); return; } if (!isValidRedirect(redirect)) { console.error("Invalid redirect URL"); return; } if (options.validateCallbackUrl && !options.validateCallbackUrl(redirect)) { console.error("Redirect URL failed custom validation"); return; } window.zerokeyParams = { publicKey, redirect, state }; if (pendingSecret) { performRedirect(pendingSecret, publicKey, redirect, state); } } function setSecret(secret) { if (!secret) { console.error("Secret cannot be empty"); return; } pendingSecret = secret; if (window.zerokeyParams) { const { publicKey, redirect, state } = window.zerokeyParams; performRedirect(secret, publicKey, redirect, state); } } exports.initSecretServer = initSecretServer; exports.setSecret = setSecret; //# sourceMappingURL=server.cjs.map //# sourceMappingURL=server.cjs.map