mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: implement cipher decryption functionality and update related API methods
This commit is contained in:
@@ -1282,6 +1282,7 @@ export default function App() {
|
||||
refetchFolders: foldersQuery.refetch,
|
||||
refetchSends: sendsQuery.refetch,
|
||||
onNotify: pushToast,
|
||||
patchDecryptedCiphers: setDecryptedCiphers,
|
||||
});
|
||||
const accountSecurityActions = useAccountSecurityActions({
|
||||
authedFetch,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
encryptZipBytesWithPassword,
|
||||
} from '@/lib/export-formats';
|
||||
import { base64ToBytes, decryptBw, decryptBwFileData, decryptStr } from '@/lib/crypto';
|
||||
import { decryptSingleCipher } from '@/lib/decrypt-cipher';
|
||||
import { t } from '@/lib/i18n';
|
||||
import {
|
||||
buildPublicSendUrl,
|
||||
@@ -66,6 +67,7 @@ interface UseVaultSendActionsOptions {
|
||||
refetchFolders: () => Promise<{ data?: VaultFolder[] | undefined } | unknown>;
|
||||
refetchSends: () => Promise<unknown>;
|
||||
onNotify: Notify;
|
||||
patchDecryptedCiphers: (updater: (prev: Cipher[]) => Cipher[]) => void;
|
||||
}
|
||||
|
||||
function extractImportIdMaps(cipherMap: ImportedCipherMapEntry[] | null) {
|
||||
@@ -95,6 +97,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
refetchFolders,
|
||||
refetchSends,
|
||||
onNotify,
|
||||
patchDecryptedCiphers,
|
||||
} = options;
|
||||
const [downloadingAttachmentKey, setDownloadingAttachmentKey] = useState('');
|
||||
const [attachmentDownloadPercent, setAttachmentDownloadPercent] = useState<number | null>(null);
|
||||
@@ -108,6 +111,29 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
await Promise.all([refetchCiphers(), refetchFolders(), refetchSends()]);
|
||||
};
|
||||
|
||||
async function decryptAndPatch(encrypted: Cipher) {
|
||||
if (!session?.symEncKey || !session?.symMacKey) {
|
||||
await refetchCiphers();
|
||||
return;
|
||||
}
|
||||
const encKey = base64ToBytes(session.symEncKey);
|
||||
const macKey = base64ToBytes(session.symMacKey);
|
||||
const decrypted = await decryptSingleCipher(encrypted, encKey, macKey);
|
||||
patchDecryptedCiphers((prev) => {
|
||||
const idx = prev.findIndex((c) => c.id === decrypted.id);
|
||||
if (idx >= 0) {
|
||||
const next = [...prev];
|
||||
next[idx] = decrypted;
|
||||
return next;
|
||||
}
|
||||
return [decrypted, ...prev];
|
||||
});
|
||||
}
|
||||
|
||||
function removeCipherFromState(id: string) {
|
||||
patchDecryptedCiphers((prev) => prev.filter((c) => c.id !== id));
|
||||
}
|
||||
|
||||
const uploadImportedAttachments = async (
|
||||
attachments: ImportAttachmentFile[],
|
||||
idMaps: { byIndex: Map<number, string>; bySourceId: Map<string, string> }
|
||||
@@ -175,7 +201,8 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
setAttachmentUploadPercent(0);
|
||||
await uploadCipherAttachment(authedFetch, session, created.id, file, undefined, setAttachmentUploadPercent);
|
||||
}
|
||||
await Promise.all([refetchCiphers(), refetchFolders()]);
|
||||
await decryptAndPatch(created);
|
||||
if (draft.folderId) await refetchFolders();
|
||||
onNotify('success', t('txt_item_created'));
|
||||
} catch (error) {
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_create_item_failed'));
|
||||
@@ -191,7 +218,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
const addFiles = Array.isArray(options?.addFiles) ? options.addFiles : [];
|
||||
const removeAttachmentIds = Array.isArray(options?.removeAttachmentIds) ? options.removeAttachmentIds : [];
|
||||
try {
|
||||
await updateCipher(authedFetch, session, cipher, draft);
|
||||
const updated = await updateCipher(authedFetch, session, cipher, draft);
|
||||
for (const attachmentId of removeAttachmentIds) {
|
||||
const id = String(attachmentId || '').trim();
|
||||
if (!id) continue;
|
||||
@@ -202,7 +229,8 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
setAttachmentUploadPercent(0);
|
||||
await uploadCipherAttachment(authedFetch, session, cipher.id, file, cipher, setAttachmentUploadPercent);
|
||||
}
|
||||
await Promise.all([refetchCiphers(), refetchFolders()]);
|
||||
await decryptAndPatch(updated);
|
||||
if (draft.folderId !== (cipher.folderId || '')) await refetchFolders();
|
||||
onNotify('success', t('txt_item_updated'));
|
||||
} catch (error) {
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_update_item_failed'));
|
||||
@@ -233,8 +261,9 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
|
||||
async deleteVaultItem(cipher: Cipher) {
|
||||
try {
|
||||
await deleteCipher(authedFetch, cipher.id);
|
||||
await Promise.all([refetchCiphers(), refetchFolders()]);
|
||||
const deleted = await deleteCipher(authedFetch, cipher.id);
|
||||
await decryptAndPatch(deleted);
|
||||
await refetchFolders();
|
||||
onNotify('success', t('txt_item_deleted'));
|
||||
} catch (error) {
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_delete_item_failed'));
|
||||
@@ -244,8 +273,9 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
|
||||
async archiveVaultItem(cipher: Cipher) {
|
||||
try {
|
||||
await archiveCipher(authedFetch, cipher.id);
|
||||
await Promise.all([refetchCiphers(), refetchFolders()]);
|
||||
const archived = await archiveCipher(authedFetch, cipher.id);
|
||||
await decryptAndPatch(archived);
|
||||
await refetchFolders();
|
||||
onNotify('success', t('txt_item_archived'));
|
||||
} catch (error) {
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_archive_item_failed'));
|
||||
@@ -255,8 +285,9 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
|
||||
async unarchiveVaultItem(cipher: Cipher) {
|
||||
try {
|
||||
await unarchiveCipher(authedFetch, cipher.id);
|
||||
await Promise.all([refetchCiphers(), refetchFolders()]);
|
||||
const unarchived = await unarchiveCipher(authedFetch, cipher.id);
|
||||
await decryptAndPatch(unarchived);
|
||||
await refetchFolders();
|
||||
onNotify('success', t('txt_item_unarchived'));
|
||||
} catch (error) {
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_unarchive_item_failed'));
|
||||
|
||||
@@ -766,7 +766,7 @@ export async function createCipher(
|
||||
authedFetch: AuthedFetch,
|
||||
session: SessionState,
|
||||
draft: VaultDraft
|
||||
): Promise<{ id: string }> {
|
||||
): Promise<Cipher> {
|
||||
const payload = await buildCipherPayload(session, draft, null);
|
||||
|
||||
const resp = await authedFetch('/api/ciphers', {
|
||||
@@ -775,9 +775,9 @@ export async function createCipher(
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) throw new Error('Create item failed');
|
||||
const body = await parseJson<{ id?: string }>(resp);
|
||||
const body = await parseJson<Cipher>(resp);
|
||||
if (!body?.id) throw new Error('Create item failed');
|
||||
return { id: body.id };
|
||||
return body;
|
||||
}
|
||||
|
||||
export async function updateCipher(
|
||||
@@ -785,7 +785,7 @@ export async function updateCipher(
|
||||
session: SessionState,
|
||||
cipher: Cipher,
|
||||
draft: VaultDraft
|
||||
): Promise<void> {
|
||||
): Promise<Cipher> {
|
||||
const payload = await buildCipherPayload(session, draft, cipher);
|
||||
|
||||
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, {
|
||||
@@ -794,25 +794,29 @@ export async function updateCipher(
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) throw new Error('Update item failed');
|
||||
return (await parseJson<Cipher>(resp))!;
|
||||
}
|
||||
|
||||
export async function deleteCipher(authedFetch: AuthedFetch, cipherId: string): Promise<void> {
|
||||
export async function deleteCipher(authedFetch: AuthedFetch, cipherId: string): Promise<Cipher> {
|
||||
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipherId)}`, { method: 'DELETE' });
|
||||
if (!resp.ok) throw new Error('Delete item failed');
|
||||
return (await parseJson<Cipher>(resp))!;
|
||||
}
|
||||
|
||||
export async function archiveCipher(authedFetch: AuthedFetch, cipherId: string): Promise<void> {
|
||||
export async function archiveCipher(authedFetch: AuthedFetch, cipherId: string): Promise<Cipher> {
|
||||
const id = String(cipherId || '').trim();
|
||||
if (!id) throw new Error('Cipher id is required');
|
||||
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(id)}/archive`, { method: 'PUT' });
|
||||
if (!resp.ok) throw new Error('Archive item failed');
|
||||
return (await parseJson<Cipher>(resp))!;
|
||||
}
|
||||
|
||||
export async function unarchiveCipher(authedFetch: AuthedFetch, cipherId: string): Promise<void> {
|
||||
export async function unarchiveCipher(authedFetch: AuthedFetch, cipherId: string): Promise<Cipher> {
|
||||
const id = String(cipherId || '').trim();
|
||||
if (!id) throw new Error('Cipher id is required');
|
||||
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(id)}/unarchive`, { method: 'PUT' });
|
||||
if (!resp.ok) throw new Error('Unarchive item failed');
|
||||
return (await parseJson<Cipher>(resp))!;
|
||||
}
|
||||
|
||||
export async function bulkDeleteCiphers(authedFetch: AuthedFetch, ids: string[]): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { decryptStr, decryptBw } from './crypto';
|
||||
import type { Cipher } from './types';
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
export async function decryptSingleCipher(
|
||||
encrypted: Cipher,
|
||||
userEnc: Uint8Array,
|
||||
userMac: Uint8Array,
|
||||
): Promise<Cipher> {
|
||||
let itemEnc = userEnc;
|
||||
let itemMac = userMac;
|
||||
if (encrypted.key) {
|
||||
try {
|
||||
const itemKey = await decryptBw(encrypted.key, userEnc, userMac);
|
||||
itemEnc = itemKey.slice(0, 32);
|
||||
itemMac = itemKey.slice(32, 64);
|
||||
} catch { /* keep user key */ }
|
||||
}
|
||||
|
||||
const decrypted: Cipher = {
|
||||
...encrypted,
|
||||
decName: await decryptField(encrypted.name, itemEnc, itemMac),
|
||||
decNotes: await decryptField(encrypted.notes, itemEnc, itemMac),
|
||||
};
|
||||
|
||||
if (encrypted.login) {
|
||||
decrypted.login = {
|
||||
...encrypted.login,
|
||||
decUsername: await decryptField(encrypted.login.username, itemEnc, itemMac),
|
||||
decPassword: await decryptField(encrypted.login.password, itemEnc, itemMac),
|
||||
decTotp: await decryptField(encrypted.login.totp, itemEnc, itemMac),
|
||||
uris: await Promise.all((encrypted.login.uris || []).map(async (u) => ({
|
||||
...u,
|
||||
decUri: await decryptField(u.uri, itemEnc, itemMac),
|
||||
}))),
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(encrypted.passwordHistory)) {
|
||||
decrypted.passwordHistory = await Promise.all(
|
||||
encrypted.passwordHistory.map(async (entry) => ({
|
||||
...entry,
|
||||
decPassword: await decryptField(entry?.password, itemEnc, itemMac),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (encrypted.card) {
|
||||
decrypted.card = {
|
||||
...encrypted.card,
|
||||
decCardholderName: await decryptField(encrypted.card.cardholderName, itemEnc, itemMac),
|
||||
decNumber: await decryptField(encrypted.card.number, itemEnc, itemMac),
|
||||
decBrand: await decryptField(encrypted.card.brand, itemEnc, itemMac),
|
||||
decExpMonth: await decryptField(encrypted.card.expMonth, itemEnc, itemMac),
|
||||
decExpYear: await decryptField(encrypted.card.expYear, itemEnc, itemMac),
|
||||
decCode: await decryptField(encrypted.card.code, itemEnc, itemMac),
|
||||
};
|
||||
}
|
||||
|
||||
if (encrypted.identity) {
|
||||
decrypted.identity = {
|
||||
...encrypted.identity,
|
||||
decTitle: await decryptField(encrypted.identity.title, itemEnc, itemMac),
|
||||
decFirstName: await decryptField(encrypted.identity.firstName, itemEnc, itemMac),
|
||||
decMiddleName: await decryptField(encrypted.identity.middleName, itemEnc, itemMac),
|
||||
decLastName: await decryptField(encrypted.identity.lastName, itemEnc, itemMac),
|
||||
decUsername: await decryptField(encrypted.identity.username, itemEnc, itemMac),
|
||||
decCompany: await decryptField(encrypted.identity.company, itemEnc, itemMac),
|
||||
decSsn: await decryptField(encrypted.identity.ssn, itemEnc, itemMac),
|
||||
decPassportNumber: await decryptField(encrypted.identity.passportNumber, itemEnc, itemMac),
|
||||
decLicenseNumber: await decryptField(encrypted.identity.licenseNumber, itemEnc, itemMac),
|
||||
decEmail: await decryptField(encrypted.identity.email, itemEnc, itemMac),
|
||||
decPhone: await decryptField(encrypted.identity.phone, itemEnc, itemMac),
|
||||
decAddress1: await decryptField(encrypted.identity.address1, itemEnc, itemMac),
|
||||
decAddress2: await decryptField(encrypted.identity.address2, itemEnc, itemMac),
|
||||
decAddress3: await decryptField(encrypted.identity.address3, itemEnc, itemMac),
|
||||
decCity: await decryptField(encrypted.identity.city, itemEnc, itemMac),
|
||||
decState: await decryptField(encrypted.identity.state, itemEnc, itemMac),
|
||||
decPostalCode: await decryptField(encrypted.identity.postalCode, itemEnc, itemMac),
|
||||
decCountry: await decryptField(encrypted.identity.country, itemEnc, itemMac),
|
||||
};
|
||||
}
|
||||
|
||||
if (encrypted.sshKey) {
|
||||
const fingerprint = encrypted.sshKey.keyFingerprint || encrypted.sshKey.fingerprint || '';
|
||||
decrypted.sshKey = {
|
||||
...encrypted.sshKey,
|
||||
decPrivateKey: await decryptField(encrypted.sshKey.privateKey, itemEnc, itemMac),
|
||||
decPublicKey: await decryptField(encrypted.sshKey.publicKey, itemEnc, itemMac),
|
||||
keyFingerprint: fingerprint || null,
|
||||
fingerprint: fingerprint || null,
|
||||
decFingerprint: await decryptField(fingerprint, itemEnc, itemMac),
|
||||
};
|
||||
}
|
||||
|
||||
if (encrypted.fields) {
|
||||
decrypted.fields = await Promise.all(
|
||||
encrypted.fields.map(async (field) => ({
|
||||
...field,
|
||||
decName: await decryptField(field.name, itemEnc, itemMac),
|
||||
decValue: await decryptField(field.value, itemEnc, itemMac),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
Reference in New Issue
Block a user