530 lines
21 KiB
JavaScript
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 };
|