mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: implement vault synchronization and decryption improvements
- Added background synchronization for vault core data, including optional folder updates. - Introduced a new API endpoint to retrieve the vault revision date. - Enhanced vault synchronization logic to utilize a caching mechanism for improved performance. - Created a new vault cache module to handle IndexedDB storage for vault core snapshots. - Implemented a worker for asynchronous decryption of vault data, improving UI responsiveness. - Updated main application settings to adjust query stale time for better data freshness. - Refactored vault-related API functions to support cache keys for more efficient data retrieval.
This commit is contained in:
@@ -518,6 +518,19 @@ export async function verifyMasterPassword(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVaultRevisionDate(authedFetch: AuthedFetch): Promise<number> {
|
||||
const resp = await authedFetch('/api/accounts/revision-date');
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to load revision date');
|
||||
}
|
||||
const body = await parseJson<number>(resp);
|
||||
const stamp = Number(body);
|
||||
if (!Number.isFinite(stamp) || stamp <= 0) {
|
||||
throw new Error('Invalid revision date');
|
||||
}
|
||||
return stamp;
|
||||
}
|
||||
|
||||
export async function getTotpStatus(authedFetch: AuthedFetch): Promise<{ enabled: boolean }> {
|
||||
const resp = await authedFetch('/api/accounts/totp');
|
||||
if (!resp.ok) throw new Error('Failed to load TOTP status');
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Cipher, Folder, Send } from '../types';
|
||||
import { getVaultRevisionDate } from './auth';
|
||||
import { loadCachedVaultCoreSnapshot, saveCachedVaultCoreSnapshot, type VaultCoreSnapshot } from '../vault-cache';
|
||||
import { parseJson, type AuthedFetch } from './shared';
|
||||
|
||||
interface VaultSyncResponse {
|
||||
@@ -7,13 +9,53 @@ interface VaultSyncResponse {
|
||||
sends?: Send[];
|
||||
}
|
||||
|
||||
const pendingVaultCoreRequests = new WeakMap<AuthedFetch, Promise<VaultSyncResponse>>();
|
||||
const pendingVaultCoreRequests = new Map<string, Promise<VaultCoreSnapshot>>();
|
||||
const memoryVaultCoreCache = new Map<string, { revisionStamp: number; snapshot: VaultCoreSnapshot }>();
|
||||
|
||||
export async function loadVaultCoreSyncSnapshot(authedFetch: AuthedFetch): Promise<VaultSyncResponse> {
|
||||
const existing = pendingVaultCoreRequests.get(authedFetch);
|
||||
function normalizeSnapshot(body: VaultSyncResponse | null | undefined): VaultCoreSnapshot {
|
||||
return {
|
||||
ciphers: Array.isArray(body?.ciphers) ? body!.ciphers! : [],
|
||||
folders: Array.isArray(body?.folders) ? body!.folders! : [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCachedVaultCoreSnapshot(cacheKey: string): Promise<VaultCoreSnapshot | null> {
|
||||
const normalizedKey = String(cacheKey || '').trim();
|
||||
if (!normalizedKey) return null;
|
||||
const memory = memoryVaultCoreCache.get(normalizedKey);
|
||||
if (memory) return memory.snapshot;
|
||||
const cached = await loadCachedVaultCoreSnapshot(normalizedKey);
|
||||
if (!cached?.snapshot) return null;
|
||||
memoryVaultCoreCache.set(normalizedKey, {
|
||||
revisionStamp: cached.revisionStamp,
|
||||
snapshot: cached.snapshot,
|
||||
});
|
||||
return cached.snapshot;
|
||||
}
|
||||
|
||||
export async function loadVaultCoreSyncSnapshot(authedFetch: AuthedFetch, cacheKey: string): Promise<VaultCoreSnapshot> {
|
||||
const normalizedKey = String(cacheKey || '').trim();
|
||||
if (!normalizedKey) return { ciphers: [], folders: [] };
|
||||
|
||||
const existing = pendingVaultCoreRequests.get(normalizedKey);
|
||||
if (existing) return existing;
|
||||
|
||||
const request = (async () => {
|
||||
const revisionStamp = await getVaultRevisionDate(authedFetch);
|
||||
const memory = memoryVaultCoreCache.get(normalizedKey);
|
||||
if (memory?.revisionStamp === revisionStamp) {
|
||||
return memory.snapshot;
|
||||
}
|
||||
|
||||
const cached = await loadCachedVaultCoreSnapshot(normalizedKey);
|
||||
if (cached?.revisionStamp === revisionStamp && cached.snapshot) {
|
||||
memoryVaultCoreCache.set(normalizedKey, {
|
||||
revisionStamp,
|
||||
snapshot: cached.snapshot,
|
||||
});
|
||||
return cached.snapshot;
|
||||
}
|
||||
|
||||
const resp = await authedFetch('/api/sync?excludeSends=true&excludeDomains=true', {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
@@ -23,15 +65,18 @@ export async function loadVaultCoreSyncSnapshot(authedFetch: AuthedFetch): Promi
|
||||
});
|
||||
if (!resp.ok) throw new Error('Failed to load vault');
|
||||
const body = await parseJson<VaultSyncResponse>(resp);
|
||||
return body || {};
|
||||
const snapshot = normalizeSnapshot(body);
|
||||
memoryVaultCoreCache.set(normalizedKey, { revisionStamp, snapshot });
|
||||
void saveCachedVaultCoreSnapshot(normalizedKey, revisionStamp, snapshot);
|
||||
return snapshot;
|
||||
})();
|
||||
|
||||
pendingVaultCoreRequests.set(authedFetch, request);
|
||||
pendingVaultCoreRequests.set(normalizedKey, request);
|
||||
try {
|
||||
return await request;
|
||||
} finally {
|
||||
if (pendingVaultCoreRequests.get(authedFetch) === request) {
|
||||
pendingVaultCoreRequests.delete(authedFetch);
|
||||
if (pendingVaultCoreRequests.get(normalizedKey) === request) {
|
||||
pendingVaultCoreRequests.delete(normalizedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
import { readResponseBytesWithProgress } from '../download';
|
||||
import { loadVaultCoreSyncSnapshot } from './vault-sync';
|
||||
|
||||
export async function getFolders(authedFetch: AuthedFetch): Promise<Folder[]> {
|
||||
const body = await loadVaultCoreSyncSnapshot(authedFetch);
|
||||
export async function getFolders(authedFetch: AuthedFetch, cacheKey: string): Promise<Folder[]> {
|
||||
const body = await loadVaultCoreSyncSnapshot(authedFetch, cacheKey);
|
||||
return body.folders || [];
|
||||
}
|
||||
|
||||
@@ -92,8 +92,8 @@ export async function updateFolder(
|
||||
if (!resp.ok) throw new Error('Update folder failed');
|
||||
}
|
||||
|
||||
export async function getCiphers(authedFetch: AuthedFetch): Promise<Cipher[]> {
|
||||
const body = await loadVaultCoreSyncSnapshot(authedFetch);
|
||||
export async function getCiphers(authedFetch: AuthedFetch, cacheKey: string): Promise<Cipher[]> {
|
||||
const body = await loadVaultCoreSyncSnapshot(authedFetch, cacheKey);
|
||||
return body.ciphers || [];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Cipher, Folder } from './types';
|
||||
|
||||
export interface VaultCoreSnapshot {
|
||||
ciphers: Cipher[];
|
||||
folders: Folder[];
|
||||
}
|
||||
|
||||
interface VaultCoreCacheRecord {
|
||||
cacheKey: string;
|
||||
revisionStamp: number;
|
||||
savedAt: number;
|
||||
snapshot: VaultCoreSnapshot;
|
||||
}
|
||||
|
||||
const DB_NAME = 'nodewarden-web-cache';
|
||||
const DB_VERSION = 1;
|
||||
const VAULT_CORE_STORE = 'vault-core';
|
||||
|
||||
let dbPromise: Promise<IDBDatabase | null> | null = null;
|
||||
|
||||
function supportsIndexedDb(): boolean {
|
||||
return typeof indexedDB !== 'undefined';
|
||||
}
|
||||
|
||||
function openDatabase(): Promise<IDBDatabase | null> {
|
||||
if (!supportsIndexedDb()) return Promise.resolve(null);
|
||||
if (dbPromise) return dbPromise;
|
||||
dbPromise = new Promise((resolve) => {
|
||||
try {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(VAULT_CORE_STORE)) {
|
||||
db.createObjectStore(VAULT_CORE_STORE, { keyPath: 'cacheKey' });
|
||||
}
|
||||
};
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => resolve(null);
|
||||
request.onblocked = () => resolve(null);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
function withStore<T>(
|
||||
mode: IDBTransactionMode,
|
||||
run: (store: IDBObjectStore) => Promise<T>
|
||||
): Promise<T | null> {
|
||||
return openDatabase().then((db) => {
|
||||
if (!db) return null;
|
||||
return new Promise<T | null>((resolve) => {
|
||||
try {
|
||||
const tx = db.transaction(VAULT_CORE_STORE, mode);
|
||||
const store = tx.objectStore(VAULT_CORE_STORE);
|
||||
void run(store).then(resolve).catch(() => resolve(null));
|
||||
tx.onerror = () => resolve(null);
|
||||
tx.onabort = () => resolve(null);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadCachedVaultCoreSnapshot(cacheKey: string): Promise<VaultCoreCacheRecord | null> {
|
||||
const normalized = String(cacheKey || '').trim();
|
||||
if (!normalized) return null;
|
||||
return withStore('readonly', (store) => new Promise<VaultCoreCacheRecord | null>((resolve) => {
|
||||
const request = store.get(normalized);
|
||||
request.onsuccess = () => {
|
||||
const record = request.result as VaultCoreCacheRecord | undefined;
|
||||
resolve(record || null);
|
||||
};
|
||||
request.onerror = () => resolve(null);
|
||||
}));
|
||||
}
|
||||
|
||||
export async function saveCachedVaultCoreSnapshot(
|
||||
cacheKey: string,
|
||||
revisionStamp: number,
|
||||
snapshot: VaultCoreSnapshot
|
||||
): Promise<void> {
|
||||
const normalized = String(cacheKey || '').trim();
|
||||
if (!normalized) return;
|
||||
await withStore('readwrite', (store) => new Promise<void>((resolve) => {
|
||||
const record: VaultCoreCacheRecord = {
|
||||
cacheKey: normalized,
|
||||
revisionStamp,
|
||||
savedAt: Date.now(),
|
||||
snapshot,
|
||||
};
|
||||
const request = store.put(record);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => resolve();
|
||||
}));
|
||||
}
|
||||
|
||||
export async function clearCachedVaultCoreSnapshot(cacheKey: string): Promise<void> {
|
||||
const normalized = String(cacheKey || '').trim();
|
||||
if (!normalized) return;
|
||||
await withStore('readwrite', (store) => new Promise<void>((resolve) => {
|
||||
const request = store.delete(normalized);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => resolve();
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { base64ToBytes, decryptBw, decryptStr, encryptBw } from './crypto';
|
||||
import { deriveSendKeyParts } from './app-support';
|
||||
import type { Cipher, Folder, Send } from './types';
|
||||
|
||||
export interface AttachmentRepairTask {
|
||||
cipherId: string;
|
||||
attachmentId: string;
|
||||
metadata: { fileName?: string; key?: string | null };
|
||||
}
|
||||
|
||||
export interface DecryptVaultCoreArgs {
|
||||
folders: Folder[];
|
||||
ciphers: Cipher[];
|
||||
symEncKeyB64: string;
|
||||
symMacKeyB64: string;
|
||||
}
|
||||
|
||||
export interface DecryptVaultCoreResult {
|
||||
folders: Folder[];
|
||||
ciphers: Cipher[];
|
||||
attachmentRepairs: AttachmentRepairTask[];
|
||||
}
|
||||
|
||||
export interface DecryptSendsArgs {
|
||||
sends: Send[];
|
||||
symEncKeyB64: string;
|
||||
symMacKeyB64: string;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
function looksLikeCipherString(value: string): boolean {
|
||||
return /^\d+\.[A-Za-z0-9+/=]+\|[A-Za-z0-9+/=]+(?:\|[A-Za-z0-9+/=]+)?$/.test(String(value || '').trim());
|
||||
}
|
||||
|
||||
function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.byteLength !== b.byteLength) return false;
|
||||
for (let i = 0; i < a.byteLength; i += 1) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function decryptField(
|
||||
value: string | null | undefined,
|
||||
enc: Uint8Array,
|
||||
mac: Uint8Array
|
||||
): Promise<string> {
|
||||
if (!value || typeof value !== 'string') return '';
|
||||
try {
|
||||
return await decryptStr(value, enc, mac);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptFieldWithSource(
|
||||
value: string | null | undefined,
|
||||
itemEnc: Uint8Array,
|
||||
itemMac: Uint8Array,
|
||||
userEnc: Uint8Array,
|
||||
userMac: Uint8Array,
|
||||
canFallbackToUserKey: boolean
|
||||
): Promise<{ text: string; source: 'item' | 'user' | 'plain' }> {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return { text: '', source: 'plain' };
|
||||
try {
|
||||
return { text: await decryptStr(raw, itemEnc, itemMac), source: 'item' };
|
||||
} catch {
|
||||
// Try legacy user-key fallback below.
|
||||
}
|
||||
if (canFallbackToUserKey) {
|
||||
try {
|
||||
return { text: await decryptStr(raw, userEnc, userMac), source: 'user' };
|
||||
} catch {
|
||||
// Keep plain fallback.
|
||||
}
|
||||
}
|
||||
return { text: raw, source: 'plain' };
|
||||
}
|
||||
|
||||
export async function decryptVaultCore(args: DecryptVaultCoreArgs): Promise<DecryptVaultCoreResult> {
|
||||
const userEnc = base64ToBytes(args.symEncKeyB64);
|
||||
const userMac = base64ToBytes(args.symMacKeyB64);
|
||||
const attachmentRepairs: AttachmentRepairTask[] = [];
|
||||
|
||||
const folders = await Promise.all(
|
||||
args.folders.map(async (folder) => ({
|
||||
...folder,
|
||||
decName: await decryptField(folder.name, userEnc, userMac),
|
||||
}))
|
||||
);
|
||||
|
||||
const ciphers = await Promise.all(
|
||||
args.ciphers.map(async (cipher) => {
|
||||
let itemEnc = userEnc;
|
||||
let itemMac = userMac;
|
||||
if (cipher.key) {
|
||||
try {
|
||||
const itemKey = await decryptBw(cipher.key, userEnc, userMac);
|
||||
itemEnc = itemKey.slice(0, 32);
|
||||
itemMac = itemKey.slice(32, 64);
|
||||
} catch {
|
||||
// Keep user key fallback.
|
||||
}
|
||||
}
|
||||
const itemUsesUserKey = sameBytes(itemEnc, userEnc) && sameBytes(itemMac, userMac);
|
||||
const nextCipher: Cipher = {
|
||||
...cipher,
|
||||
decName: await decryptField(cipher.name || '', itemEnc, itemMac),
|
||||
decNotes: await decryptField(cipher.notes || '', itemEnc, itemMac),
|
||||
};
|
||||
|
||||
if (cipher.login) {
|
||||
nextCipher.login = {
|
||||
...cipher.login,
|
||||
decUsername: await decryptField(cipher.login.username || '', itemEnc, itemMac),
|
||||
decPassword: await decryptField(cipher.login.password || '', itemEnc, itemMac),
|
||||
decTotp: await decryptField(cipher.login.totp || '', itemEnc, itemMac),
|
||||
uris: await Promise.all(
|
||||
(cipher.login.uris || []).map(async (uri) => ({
|
||||
...uri,
|
||||
decUri: await decryptField(uri.uri || '', itemEnc, itemMac),
|
||||
}))
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(cipher.passwordHistory)) {
|
||||
nextCipher.passwordHistory = await Promise.all(
|
||||
cipher.passwordHistory.map(async (entry) => ({
|
||||
...entry,
|
||||
decPassword: await decryptField(entry?.password || '', itemEnc, itemMac),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (cipher.card) {
|
||||
nextCipher.card = {
|
||||
...cipher.card,
|
||||
decCardholderName: await decryptField(cipher.card.cardholderName || '', itemEnc, itemMac),
|
||||
decNumber: await decryptField(cipher.card.number || '', itemEnc, itemMac),
|
||||
decBrand: await decryptField(cipher.card.brand || '', itemEnc, itemMac),
|
||||
decExpMonth: await decryptField(cipher.card.expMonth || '', itemEnc, itemMac),
|
||||
decExpYear: await decryptField(cipher.card.expYear || '', itemEnc, itemMac),
|
||||
decCode: await decryptField(cipher.card.code || '', itemEnc, itemMac),
|
||||
};
|
||||
}
|
||||
|
||||
if (cipher.identity) {
|
||||
nextCipher.identity = {
|
||||
...cipher.identity,
|
||||
decTitle: await decryptField(cipher.identity.title || '', itemEnc, itemMac),
|
||||
decFirstName: await decryptField(cipher.identity.firstName || '', itemEnc, itemMac),
|
||||
decMiddleName: await decryptField(cipher.identity.middleName || '', itemEnc, itemMac),
|
||||
decLastName: await decryptField(cipher.identity.lastName || '', itemEnc, itemMac),
|
||||
decUsername: await decryptField(cipher.identity.username || '', itemEnc, itemMac),
|
||||
decCompany: await decryptField(cipher.identity.company || '', itemEnc, itemMac),
|
||||
decSsn: await decryptField(cipher.identity.ssn || '', itemEnc, itemMac),
|
||||
decPassportNumber: await decryptField(cipher.identity.passportNumber || '', itemEnc, itemMac),
|
||||
decLicenseNumber: await decryptField(cipher.identity.licenseNumber || '', itemEnc, itemMac),
|
||||
decEmail: await decryptField(cipher.identity.email || '', itemEnc, itemMac),
|
||||
decPhone: await decryptField(cipher.identity.phone || '', itemEnc, itemMac),
|
||||
decAddress1: await decryptField(cipher.identity.address1 || '', itemEnc, itemMac),
|
||||
decAddress2: await decryptField(cipher.identity.address2 || '', itemEnc, itemMac),
|
||||
decAddress3: await decryptField(cipher.identity.address3 || '', itemEnc, itemMac),
|
||||
decCity: await decryptField(cipher.identity.city || '', itemEnc, itemMac),
|
||||
decState: await decryptField(cipher.identity.state || '', itemEnc, itemMac),
|
||||
decPostalCode: await decryptField(cipher.identity.postalCode || '', itemEnc, itemMac),
|
||||
decCountry: await decryptField(cipher.identity.country || '', itemEnc, itemMac),
|
||||
};
|
||||
}
|
||||
|
||||
if (cipher.sshKey) {
|
||||
const encryptedFingerprint = cipher.sshKey.keyFingerprint || cipher.sshKey.fingerprint || '';
|
||||
nextCipher.sshKey = {
|
||||
...cipher.sshKey,
|
||||
decPrivateKey: await decryptField(cipher.sshKey.privateKey || '', itemEnc, itemMac),
|
||||
decPublicKey: await decryptField(cipher.sshKey.publicKey || '', itemEnc, itemMac),
|
||||
keyFingerprint: encryptedFingerprint || null,
|
||||
fingerprint: encryptedFingerprint || null,
|
||||
decFingerprint: await decryptField(encryptedFingerprint, itemEnc, itemMac),
|
||||
};
|
||||
}
|
||||
|
||||
if (cipher.fields) {
|
||||
nextCipher.fields = await Promise.all(
|
||||
cipher.fields.map(async (field) => ({
|
||||
...field,
|
||||
decName: await decryptField(field.name || '', itemEnc, itemMac),
|
||||
decValue: await decryptField(field.value || '', itemEnc, itemMac),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(cipher.attachments)) {
|
||||
nextCipher.attachments = await Promise.all(
|
||||
cipher.attachments.map(async (attachment) => {
|
||||
const attachmentId = String(attachment?.id || '').trim();
|
||||
const fileNameResult = await decryptFieldWithSource(
|
||||
attachment.fileName || '',
|
||||
itemEnc,
|
||||
itemMac,
|
||||
userEnc,
|
||||
userMac,
|
||||
!itemUsesUserKey
|
||||
);
|
||||
const metadata: { fileName?: string; key?: string | null } = {};
|
||||
|
||||
if (attachmentId && fileNameResult.source === 'user') {
|
||||
metadata.fileName = await encryptBw(new TextEncoder().encode(fileNameResult.text), itemEnc, itemMac);
|
||||
}
|
||||
|
||||
const attachmentKey = String(attachment?.key || '').trim();
|
||||
if (attachmentId && attachmentKey && looksLikeCipherString(attachmentKey) && !itemUsesUserKey) {
|
||||
try {
|
||||
await decryptBw(attachmentKey, itemEnc, itemMac);
|
||||
} catch {
|
||||
try {
|
||||
const rawAttachmentKey = await decryptBw(attachmentKey, userEnc, userMac);
|
||||
if (rawAttachmentKey.length >= 64) {
|
||||
metadata.key = await encryptBw(rawAttachmentKey, itemEnc, itemMac);
|
||||
}
|
||||
} catch {
|
||||
// Download path still supports legacy format.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attachmentId && Object.keys(metadata).length > 0) {
|
||||
attachmentRepairs.push({
|
||||
cipherId: cipher.id,
|
||||
attachmentId,
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
decFileName: fileNameResult.text,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return nextCipher;
|
||||
})
|
||||
);
|
||||
|
||||
return { folders, ciphers, attachmentRepairs };
|
||||
}
|
||||
|
||||
export async function decryptSends(args: DecryptSendsArgs): Promise<Send[]> {
|
||||
const userEnc = base64ToBytes(args.symEncKeyB64);
|
||||
const userMac = base64ToBytes(args.symMacKeyB64);
|
||||
return Promise.all(
|
||||
args.sends.map(async (send) => {
|
||||
const nextSend: Send = { ...send };
|
||||
try {
|
||||
if (send.key) {
|
||||
const sendKeyRaw = await decryptBw(send.key, userEnc, userMac);
|
||||
const derived = await deriveSendKeyParts(sendKeyRaw);
|
||||
nextSend.decName = await decryptField(send.name || '', derived.enc, derived.mac);
|
||||
nextSend.decNotes = await decryptField(send.notes || '', derived.enc, derived.mac);
|
||||
nextSend.decText = await decryptField(send.text?.text || '', derived.enc, derived.mac);
|
||||
if (send.file?.fileName) {
|
||||
const decFileName = await decryptField(send.file.fileName, derived.enc, derived.mac);
|
||||
nextSend.file = {
|
||||
...(send.file || {}),
|
||||
fileName: decFileName || send.file.fileName,
|
||||
};
|
||||
}
|
||||
nextSend.decShareKey = btoa(String.fromCharCode(...sendKeyRaw))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/g, '');
|
||||
nextSend.shareUrl = `${args.origin}/#/send/${send.accessId}/${nextSend.decShareKey}`;
|
||||
} else {
|
||||
nextSend.decName = '';
|
||||
nextSend.decNotes = '';
|
||||
nextSend.decText = '';
|
||||
}
|
||||
} catch {
|
||||
nextSend.decName = 'Decrypt failed';
|
||||
}
|
||||
return nextSend;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Send } from './types';
|
||||
import type { DecryptSendsArgs, DecryptVaultCoreArgs, DecryptVaultCoreResult } from './vault-decrypt';
|
||||
|
||||
type WorkerSuccess<T> = { id: number; ok: true; result: T };
|
||||
type WorkerFailure = { id: number; ok: false; error: string };
|
||||
type WorkerResponse<T> = WorkerSuccess<T> | WorkerFailure;
|
||||
|
||||
let worker: Worker | null = null;
|
||||
let nextJobId = 1;
|
||||
const pending = new Map<number, { resolve: (value: any) => void; reject: (error: Error) => void }>();
|
||||
|
||||
function getWorker(): Worker | null {
|
||||
if (typeof Worker === 'undefined') return null;
|
||||
if (worker) return worker;
|
||||
worker = new Worker(new URL('../workers/vault-decrypt.worker.ts', import.meta.url), { type: 'module' });
|
||||
worker.addEventListener('message', (event: MessageEvent<WorkerResponse<unknown>>) => {
|
||||
const message = event.data;
|
||||
const job = pending.get(message.id);
|
||||
if (!job) return;
|
||||
pending.delete(message.id);
|
||||
if (message.ok) {
|
||||
job.resolve(message.result);
|
||||
return;
|
||||
}
|
||||
job.reject(new Error(message.error || 'Decrypt failed'));
|
||||
});
|
||||
worker.addEventListener('error', () => {
|
||||
for (const [, job] of pending) {
|
||||
job.reject(new Error('Decrypt worker failed'));
|
||||
}
|
||||
pending.clear();
|
||||
worker = null;
|
||||
});
|
||||
return worker;
|
||||
}
|
||||
|
||||
function postJob<T>(payload: { kind: 'vault-core'; payload: DecryptVaultCoreArgs } | { kind: 'sends'; payload: DecryptSendsArgs }): Promise<T> {
|
||||
const instance = getWorker();
|
||||
if (!instance) {
|
||||
return Promise.reject(new Error('Decrypt worker unavailable'));
|
||||
}
|
||||
const id = nextJobId++;
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
instance.postMessage({ id, ...payload });
|
||||
});
|
||||
}
|
||||
|
||||
export function decryptVaultCoreInWorker(args: DecryptVaultCoreArgs): Promise<DecryptVaultCoreResult> {
|
||||
return postJob<DecryptVaultCoreResult>({ kind: 'vault-core', payload: args });
|
||||
}
|
||||
|
||||
export function decryptSendsInWorker(args: DecryptSendsArgs): Promise<Send[]> {
|
||||
return postJob<Send[]>({ kind: 'sends', payload: args });
|
||||
}
|
||||
Reference in New Issue
Block a user