zerokey
Version:
Zero-knowledge cross-domain secret sharing library using ECDH encryption
165 lines (162 loc) • 4.8 kB
JavaScript
;
// 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