rswag-online/frontend/vendor/@encryptid/sdk/index-24r9wkfe.js

530 lines
21 KiB
JavaScript

import {
bufferToBase64url
} from "./index-2cp5044h.js";
import {
AuthLevel
} from "./index-5c1t4ftn.js";
// src/client/key-derivation.ts
class EncryptIDKeyManager {
masterKey = null;
derivedKeys = null;
fromPRF = false;
async initFromPRF(prfOutput) {
this.masterKey = await crypto.subtle.importKey("raw", prfOutput, { name: "HKDF" }, false, ["deriveKey", "deriveBits"]);
this.fromPRF = true;
this.derivedKeys = null;
}
async initFromPassphrase(passphrase, salt) {
const encoder = new TextEncoder;
const passphraseKey = await crypto.subtle.importKey("raw", encoder.encode(passphrase), { name: "PBKDF2" }, false, ["deriveBits"]);
const masterKeyMaterial = await crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 600000, hash: "SHA-256" }, passphraseKey, 256);
this.masterKey = await crypto.subtle.importKey("raw", masterKeyMaterial, { name: "HKDF" }, false, ["deriveKey", "deriveBits"]);
this.fromPRF = false;
this.derivedKeys = null;
}
static generateSalt() {
return crypto.getRandomValues(new Uint8Array(32));
}
isInitialized() {
return this.masterKey !== null;
}
async getKeys() {
if (!this.masterKey)
throw new Error("Key manager not initialized");
if (this.derivedKeys)
return this.derivedKeys;
const [encryptionKey, signingKeyPair, didSeed] = await Promise.all([
this.deriveEncryptionKey(),
this.deriveSigningKeyPair(),
this.deriveDIDSeed()
]);
const did = await this.generateDID(didSeed);
this.derivedKeys = { encryptionKey, signingKeyPair, didSeed, did, fromPRF: this.fromPRF };
return this.derivedKeys;
}
async deriveEncryptionKey() {
const encoder = new TextEncoder;
return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: encoder.encode("encryptid-encryption-key-v1"), info: encoder.encode("AES-256-GCM") }, this.masterKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt", "wrapKey", "unwrapKey"]);
}
async deriveSigningKeyPair() {
return crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]);
}
async deriveDIDSeed() {
const encoder = new TextEncoder;
const seed = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: encoder.encode("encryptid-did-key-v1"), info: encoder.encode("Ed25519-seed") }, this.masterKey, 256);
return new Uint8Array(seed);
}
async generateDID(seed) {
const publicKeyHash = await crypto.subtle.digest("SHA-256", seed);
const publicKeyBytes = new Uint8Array(publicKeyHash).slice(0, 32);
const multicodecPrefix = new Uint8Array([237, 1]);
const multicodecKey = new Uint8Array(34);
multicodecKey.set(multicodecPrefix);
multicodecKey.set(publicKeyBytes, 2);
const base58Encoded = bufferToBase64url(multicodecKey.buffer).replace(/-/g, "").replace(/_/g, "");
return `did:key:z${base58Encoded}`;
}
clear() {
this.masterKey = null;
this.derivedKeys = null;
this.fromPRF = false;
}
}
async function encryptData(key, data) {
let plaintext;
if (typeof data === "string")
plaintext = new TextEncoder().encode(data).buffer;
else if (data instanceof Uint8Array)
plaintext = data.buffer;
else
plaintext = data;
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
return { ciphertext, iv };
}
async function decryptData(key, encrypted) {
return crypto.subtle.decrypt({ name: "AES-GCM", iv: encrypted.iv }, key, encrypted.ciphertext);
}
async function decryptDataAsString(key, encrypted) {
return new TextDecoder().decode(await decryptData(key, encrypted));
}
async function signData(keyPair, data) {
let dataBuffer;
if (typeof data === "string")
dataBuffer = new TextEncoder().encode(data).buffer;
else if (data instanceof Uint8Array)
dataBuffer = data.buffer;
else
dataBuffer = data;
const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, keyPair.privateKey, dataBuffer);
const publicKey = await crypto.subtle.exportKey("raw", keyPair.publicKey);
return { data: dataBuffer, signature, publicKey };
}
async function verifySignature(signed) {
const publicKey = await crypto.subtle.importKey("raw", signed.publicKey, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
return crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, publicKey, signed.signature, signed.data);
}
async function wrapKeyForRecipient(keyToWrap, recipientPublicKey) {
return crypto.subtle.wrapKey("raw", keyToWrap, recipientPublicKey, { name: "RSA-OAEP" });
}
async function unwrapSharedKey(wrappedKey, privateKey) {
return crypto.subtle.unwrapKey("raw", wrappedKey, privateKey, { name: "RSA-OAEP" }, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
}
var keyManagerInstance = null;
function getKeyManager() {
if (!keyManagerInstance)
keyManagerInstance = new EncryptIDKeyManager;
return keyManagerInstance;
}
function resetKeyManager() {
if (keyManagerInstance) {
keyManagerInstance.clear();
keyManagerInstance = null;
}
}
// src/client/session.ts
var OPERATION_PERMISSIONS = {
"rspace:view-public": { minAuthLevel: 1 /* BASIC */ },
"rspace:view-private": { minAuthLevel: 2 /* STANDARD */ },
"rspace:edit-board": { minAuthLevel: 2 /* STANDARD */ },
"rspace:create-board": { minAuthLevel: 2 /* STANDARD */ },
"rspace:delete-board": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rspace:encrypt-board": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "encrypt" },
"rwallet:view-balance": { minAuthLevel: 1 /* BASIC */ },
"rwallet:view-history": { minAuthLevel: 2 /* STANDARD */ },
"rwallet:send-small": { minAuthLevel: 2 /* STANDARD */, requiresCapability: "wallet" },
"rwallet:send-large": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "wallet", maxAgeSeconds: 60 },
"rwallet:add-guardian": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rwallet:remove-guardian": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rvote:view-proposals": { minAuthLevel: 1 /* BASIC */ },
"rvote:cast-vote": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "sign", maxAgeSeconds: 300 },
"rvote:delegate": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "wallet" },
"rfiles:list-files": { minAuthLevel: 2 /* STANDARD */ },
"rfiles:download-own": { minAuthLevel: 2 /* STANDARD */, requiresCapability: "encrypt" },
"rfiles:upload": { minAuthLevel: 2 /* STANDARD */, requiresCapability: "encrypt" },
"rfiles:share": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "encrypt" },
"rfiles:delete": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rfiles:export-keys": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rmaps:view-public": { minAuthLevel: 1 /* BASIC */ },
"rmaps:add-location": { minAuthLevel: 2 /* STANDARD */ },
"rmaps:edit-location": { minAuthLevel: 2 /* STANDARD */, requiresCapability: "sign" },
"account:view-profile": { minAuthLevel: 2 /* STANDARD */ },
"account:edit-profile": { minAuthLevel: 3 /* ELEVATED */ },
"account:export-data": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"account:delete": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rspace:create-space": { minAuthLevel: 2 /* STANDARD */ },
"rspace:configure-space": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rspace:delete-space": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rspace:invite-member": { minAuthLevel: 2 /* STANDARD */ },
"rspace:remove-member": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rspace:change-visibility": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rfunds:create-space": { minAuthLevel: 2 /* STANDARD */ },
"rfunds:edit-flows": { minAuthLevel: 2 /* STANDARD */ },
"rfunds:share-space": { minAuthLevel: 2 /* STANDARD */ }
};
var SESSION_STORAGE_KEY = "encryptid_session";
var TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000;
class SessionManager {
session = null;
refreshTimer = null;
constructor() {
this.restoreSession();
}
async createSession(authResult, did, capabilities) {
const now = Math.floor(Date.now() / 1000);
const claims = {
iss: "https://encryptid.jeffemmett.com",
sub: did,
aud: ["rspace.online", "rwallet.online", "rvote.online", "rfiles.online", "rmaps.online"],
iat: now,
exp: now + 15 * 60,
jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
eid: {
credentialId: authResult.credentialId,
authLevel: 3 /* ELEVATED */,
authTime: now,
capabilities,
recoveryConfigured: false
}
};
const accessToken = this.createUnsignedToken(claims);
const refreshToken = this.createRefreshToken(did);
this.session = { accessToken, refreshToken, claims, lastAuthTime: Date.now() };
this.persistSession();
this.scheduleRefresh();
return this.session;
}
getSession() {
return this.session;
}
getDID() {
return this.session?.claims.sub ?? null;
}
getAccessToken() {
return this.session?.accessToken ?? null;
}
getAuthLevel() {
if (!this.session)
return 1 /* BASIC */;
const now = Math.floor(Date.now() / 1000);
if (now >= this.session.claims.exp)
return 1 /* BASIC */;
const authAge = now - this.session.claims.eid.authTime;
if (authAge < 60)
return 3 /* ELEVATED */;
if (authAge < 15 * 60)
return 2 /* STANDARD */;
return 1 /* BASIC */;
}
canPerform(operation) {
const permission = OPERATION_PERMISSIONS[operation];
if (!permission)
return { allowed: false, reason: "Unknown operation" };
if (!this.session)
return { allowed: false, reason: "Not authenticated" };
const currentLevel = this.getAuthLevel();
if (currentLevel < permission.minAuthLevel) {
return { allowed: false, reason: `Requires ${AuthLevel[permission.minAuthLevel]} auth level (current: ${AuthLevel[currentLevel]})` };
}
if (permission.requiresCapability) {
if (!this.session.claims.eid.capabilities[permission.requiresCapability]) {
return { allowed: false, reason: `Requires ${permission.requiresCapability} capability` };
}
}
if (permission.maxAgeSeconds) {
const authAge = Math.floor(Date.now() / 1000) - this.session.claims.eid.authTime;
if (authAge > permission.maxAgeSeconds) {
return { allowed: false, reason: `Authentication too old (${authAge}s > ${permission.maxAgeSeconds}s)` };
}
}
return { allowed: true };
}
requiresFreshAuth(operation) {
const permission = OPERATION_PERMISSIONS[operation];
if (!permission)
return true;
if (permission.minAuthLevel >= 4 /* CRITICAL */)
return true;
if (permission.maxAgeSeconds && permission.maxAgeSeconds <= 60)
return true;
return false;
}
upgradeAuthLevel(level = 3 /* ELEVATED */) {
if (!this.session)
return;
this.session.claims.eid.authLevel = level;
this.session.claims.eid.authTime = Math.floor(Date.now() / 1000);
this.session.lastAuthTime = Date.now();
this.persistSession();
}
clearSession() {
this.session = null;
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
try {
localStorage.removeItem(SESSION_STORAGE_KEY);
} catch {}
}
isValid() {
if (!this.session)
return false;
return Math.floor(Date.now() / 1000) < this.session.claims.exp;
}
createUnsignedToken(claims) {
const header = { alg: "none", typ: "JWT" };
return `${btoa(JSON.stringify(header))}.${btoa(JSON.stringify(claims))}.`;
}
createRefreshToken(did) {
return btoa(JSON.stringify({
sub: did,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer)
}));
}
persistSession() {
if (!this.session)
return;
try {
localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(this.session));
} catch {}
}
restoreSession() {
try {
const stored = localStorage.getItem(SESSION_STORAGE_KEY);
if (stored) {
const session = JSON.parse(stored);
if (Math.floor(Date.now() / 1000) < session.claims.exp) {
this.session = session;
this.scheduleRefresh();
} else {
localStorage.removeItem(SESSION_STORAGE_KEY);
}
}
} catch {}
}
scheduleRefresh() {
if (!this.session)
return;
if (this.refreshTimer)
clearTimeout(this.refreshTimer);
const expiresAt = this.session.claims.exp * 1000;
const refreshAt = expiresAt - TOKEN_REFRESH_THRESHOLD;
const delay = Math.max(refreshAt - Date.now(), 0);
this.refreshTimer = setTimeout(() => this.refreshTokens(), delay);
}
async refreshTokens() {
if (!this.session)
return;
const now = Math.floor(Date.now() / 1000);
this.session.claims.eid.authLevel = Math.min(this.session.claims.eid.authLevel, 2 /* STANDARD */);
this.session.claims.iat = now;
this.session.claims.exp = now + 15 * 60;
this.session.claims.jti = bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer);
this.session.accessToken = this.createUnsignedToken(this.session.claims);
this.persistSession();
this.scheduleRefresh();
}
}
var sessionManagerInstance = null;
function getSessionManager() {
if (!sessionManagerInstance)
sessionManagerInstance = new SessionManager;
return sessionManagerInstance;
}
// src/client/recovery.ts
class RecoveryManager {
config = null;
activeRequest = null;
constructor() {
this.loadConfig();
}
async initializeRecovery(threshold = 3) {
this.config = {
threshold,
delaySeconds: 48 * 60 * 60,
guardians: [],
guardianListHash: "",
updatedAt: Date.now()
};
await this.saveConfig();
return this.config;
}
async addGuardian(guardian) {
if (!this.config)
throw new Error("Recovery not initialized");
if (this.config.guardians.length >= 7)
throw new Error("Maximum of 7 guardians allowed");
const newGuardian = {
...guardian,
id: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
addedAt: Date.now()
};
this.config.guardians.push(newGuardian);
this.config.guardianListHash = await this.hashGuardianList();
this.config.updatedAt = Date.now();
await this.saveConfig();
return newGuardian;
}
async removeGuardian(guardianId) {
if (!this.config)
throw new Error("Recovery not initialized");
const index = this.config.guardians.findIndex((g) => g.id === guardianId);
if (index === -1)
throw new Error("Guardian not found");
const remainingWeight = this.config.guardians.filter((g) => g.id !== guardianId).reduce((sum, g) => sum + g.weight, 0);
if (remainingWeight < this.config.threshold)
throw new Error("Cannot remove guardian: would make recovery impossible");
this.config.guardians.splice(index, 1);
this.config.guardianListHash = await this.hashGuardianList();
this.config.updatedAt = Date.now();
await this.saveConfig();
}
async setThreshold(threshold) {
if (!this.config)
throw new Error("Recovery not initialized");
const totalWeight = this.config.guardians.reduce((sum, g) => sum + g.weight, 0);
if (threshold > totalWeight)
throw new Error("Threshold cannot exceed total guardian weight");
if (threshold < 1)
throw new Error("Threshold must be at least 1");
this.config.threshold = threshold;
this.config.updatedAt = Date.now();
await this.saveConfig();
}
async setDelay(delaySeconds) {
if (!this.config)
throw new Error("Recovery not initialized");
if (delaySeconds < 3600 || delaySeconds > 7 * 24 * 3600)
throw new Error("Delay must be between 1 hour and 7 days");
this.config.delaySeconds = delaySeconds;
this.config.updatedAt = Date.now();
await this.saveConfig();
}
getConfig() {
return this.config;
}
isConfigured() {
if (!this.config)
return false;
return this.config.guardians.reduce((sum, g) => sum + g.weight, 0) >= this.config.threshold;
}
async verifyGuardian(guardianId) {
if (!this.config)
throw new Error("Recovery not initialized");
const guardian = this.config.guardians.find((g) => g.id === guardianId);
if (!guardian)
throw new Error("Guardian not found");
guardian.lastVerified = Date.now();
await this.saveConfig();
return true;
}
async initiateRecovery(newCredentialId) {
if (!this.config)
throw new Error("Recovery not configured");
if (this.activeRequest?.status === "pending")
throw new Error("Recovery already in progress");
const now = Date.now();
this.activeRequest = {
id: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
accountDID: "",
newCredentialId,
initiatedAt: now,
completesAt: now + this.config.delaySeconds * 1000,
status: "pending",
approvals: [],
approvalWeight: 0
};
return this.activeRequest;
}
async approveRecovery(guardianId, signature) {
if (!this.activeRequest || this.activeRequest.status !== "pending")
throw new Error("No pending recovery request");
if (!this.config)
throw new Error("Recovery not configured");
const guardian = this.config.guardians.find((g) => g.id === guardianId);
if (!guardian)
throw new Error("Guardian not found");
if (this.activeRequest.approvals.some((a) => a.guardianId === guardianId))
throw new Error("Guardian already approved");
this.activeRequest.approvals.push({ guardianId, approvedAt: Date.now(), signature });
this.activeRequest.approvalWeight += guardian.weight;
if (this.activeRequest.approvalWeight >= this.config.threshold) {
this.activeRequest.status = "approved";
}
return this.activeRequest;
}
async cancelRecovery() {
if (!this.activeRequest || this.activeRequest.status !== "pending")
throw new Error("No pending recovery request to cancel");
this.activeRequest.status = "cancelled";
this.activeRequest = null;
}
async completeRecovery() {
if (!this.activeRequest)
throw new Error("No recovery request");
if (this.activeRequest.status !== "approved")
throw new Error("Recovery not approved");
if (Date.now() < this.activeRequest.completesAt) {
const remaining = this.activeRequest.completesAt - Date.now();
throw new Error(`Time-lock not expired. ${Math.ceil(remaining / 1000 / 60)} minutes remaining.`);
}
this.activeRequest.status = "completed";
this.activeRequest = null;
}
getActiveRequest() {
return this.activeRequest;
}
async hashGuardianList() {
if (!this.config)
return "";
const sortedIds = this.config.guardians.map((g) => g.id).sort().join(",");
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(sortedIds));
return bufferToBase64url(hash);
}
async saveConfig() {
if (!this.config)
return;
try {
localStorage.setItem("encryptid_recovery", JSON.stringify(this.config));
} catch {}
}
loadConfig() {
try {
const stored = localStorage.getItem("encryptid_recovery");
if (stored)
this.config = JSON.parse(stored);
} catch {}
}
}
var recoveryManagerInstance = null;
function getRecoveryManager() {
if (!recoveryManagerInstance)
recoveryManagerInstance = new RecoveryManager;
return recoveryManagerInstance;
}
function getGuardianTypeInfo(type) {
switch (type) {
case "secondary_passkey" /* SECONDARY_PASSKEY */:
return { name: "Backup Passkey", description: "Another device you own (phone, YubiKey, etc.)", icon: "key", setupInstructions: "Register a passkey on a second device you control." };
case "trusted_contact" /* TRUSTED_CONTACT */:
return { name: "Trusted Contact", description: "A friend or family member with their own EncryptID", icon: "user", setupInstructions: "Ask a trusted person to create an EncryptID account." };
case "hardware_key" /* HARDWARE_KEY */:
return { name: "Hardware Security Key", description: "A YubiKey or similar device stored offline", icon: "shield", setupInstructions: "Register a hardware security key and store it safely." };
case "institutional" /* INSTITUTIONAL */:
return { name: "Recovery Service", description: "A professional recovery service provider", icon: "building", setupInstructions: "Connect with a trusted recovery service." };
case "time_delayed_self" /* TIME_DELAYED_SELF */:
return { name: "Time-Delayed Self", description: "Recover yourself after a waiting period", icon: "clock", setupInstructions: "Set up a recovery option that requires waiting before completing." };
default:
return { name: "Unknown", description: "Unknown guardian type", icon: "question", setupInstructions: "" };
}
}
export { EncryptIDKeyManager, encryptData, decryptData, decryptDataAsString, signData, verifySignature, wrapKeyForRecipient, unwrapSharedKey, getKeyManager, resetKeyManager, OPERATION_PERMISSIONS, SessionManager, getSessionManager, RecoveryManager, getRecoveryManager, getGuardianTypeInfo };