3.14 Password Vault encryption AES-256-GCM server-side

- Created src/core/crypto/ with AES-256-GCM encrypt/decrypt (PBKDF2 key derivation)
- Created /api/vault route: CRUD with server-side password encryption
- PATCH /api/vault migration endpoint to re-encrypt legacy plaintext passwords
- Rewrote use-vault hook to use dedicated /api/vault instead of generic storage
- Updated UI: amber 'not encrypted' warning  green 'encrypted' badge
- Added ENCRYPTION_SECRET env var to docker-compose.yml and stack.env
- Module version bumped to 0.2.0
This commit is contained in:
AI Assistant
2026-02-28 04:12:44 +02:00
parent f0b3659247
commit 85bdb59da4
10 changed files with 366 additions and 59 deletions
@@ -169,9 +169,9 @@ export function PasswordVaultModule() {
return (
<div className="space-y-6">
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-2 text-xs text-amber-700 dark:text-amber-400">
Atenție: Parolele sunt stocate local (localStorage). Nu sunt criptate.
Folosiți un manager de parole dedicat pentru date sensibile.
<div className="rounded-md border border-emerald-500/30 bg-emerald-500/5 px-4 py-2 text-xs text-emerald-700 dark:text-emerald-400">
Parolele sunt criptate (AES-256-GCM) pe server înainte de stocare.
Datele sunt protejate la rest în baza de date.
</div>
{/* Stats */}
+13 -12
View File
@@ -1,17 +1,18 @@
import type { ModuleConfig } from '@/core/module-registry/types';
import type { ModuleConfig } from "@/core/module-registry/types";
export const passwordVaultConfig: ModuleConfig = {
id: 'password-vault',
name: 'Seif Parole',
description: 'Manager securizat de parole și credențiale cu criptare locală',
icon: 'lock',
route: '/password-vault',
category: 'operations',
featureFlag: 'module.password-vault',
visibility: 'admin',
version: '0.1.0',
id: "password-vault",
name: "Seif Parole",
description:
"Manager securizat de parole și credențiale cu criptare AES-256-GCM",
icon: "lock",
route: "/password-vault",
category: "operations",
featureFlag: "module.password-vault",
visibility: "admin",
version: "0.2.0",
dependencies: [],
storageNamespace: 'password-vault',
storageNamespace: "password-vault",
navOrder: 11,
tags: ['parole', 'securitate', 'credențiale'],
tags: ["parole", "securitate", "credențiale", "criptare"],
};
+32 -21
View File
@@ -1,19 +1,19 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useStorage } from "@/core/storage";
import { v4 as uuid } from "uuid";
import type { VaultEntry, VaultEntryCategory } from "../types";
const PREFIX = "vault:";
export interface VaultFilters {
search: string;
category: VaultEntryCategory | "all";
}
/**
* Vault hook — uses dedicated /api/vault endpoint with server-side encryption.
* Passwords are encrypted at rest (AES-256-GCM) and decrypted only on read.
*/
export function useVault() {
const storage = useStorage("password-vault");
const [entries, setEntries] = useState<VaultEntry[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<VaultFilters>({
@@ -23,17 +23,18 @@ export function useVault() {
const refresh = useCallback(async () => {
setLoading(true);
const all = await storage.exportAll();
const results: VaultEntry[] = [];
for (const [key, value] of Object.entries(all)) {
if (key.startsWith(PREFIX) && value) {
results.push(value as VaultEntry);
}
try {
const res = await fetch("/api/vault");
const data = await res.json();
const results = (data.entries ?? []) as VaultEntry[];
results.sort((a, b) => a.label.localeCompare(b.label));
setEntries(results);
} catch (err) {
console.error("Failed to load vault entries:", err);
} finally {
setLoading(false);
}
results.sort((a, b) => a.label.localeCompare(b.label));
setEntries(results);
setLoading(false);
}, [storage]);
}, []);
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => {
@@ -49,36 +50,46 @@ export function useVault() {
createdAt: now,
updatedAt: now,
};
await storage.set(`${PREFIX}${entry.id}`, entry);
await fetch("/api/vault", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entry }),
});
await refresh();
return entry;
},
[storage, refresh],
[refresh],
);
const updateEntry = useCallback(
async (id: string, updates: Partial<VaultEntry>) => {
const existing = entries.find((e) => e.id === id);
if (!existing) return;
const updated = {
const updated: VaultEntry = {
...existing,
...updates,
id: existing.id,
createdAt: existing.createdAt,
updatedAt: new Date().toISOString(),
};
await storage.set(`${PREFIX}${id}`, updated);
await fetch("/api/vault", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entry: updated }),
});
await refresh();
},
[storage, refresh, entries],
[refresh, entries],
);
const removeEntry = useCallback(
async (id: string) => {
await storage.delete(`${PREFIX}${id}`);
await fetch(`/api/vault?id=${encodeURIComponent(id)}`, {
method: "DELETE",
});
await refresh();
},
[storage, refresh],
[refresh],
);
const updateFilter = useCallback(