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:
@@ -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 */}
|
||||
|
||||
@@ -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"],
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user