Skip to content

Commit cd5bbe0

Browse files
committed
Implemented randomly generated encryption keys + IVs
1 parent 23952d9 commit cd5bbe0

File tree

6 files changed

+217
-65
lines changed

6 files changed

+217
-65
lines changed

prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ model PasswordItem {
3838
website String
3939
username String
4040
password String
41+
usernameIV String
42+
websiteIV String
43+
passwordIV String
4144
createdAt DateTime @default(now())
4245
updatedAt DateTime @updatedAt
4346
userId String

src/app/actions.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
"use server";
22

33
import prismadb from "@/lib/prismadb";
4-
import { auth, currentUser } from "@clerk/nextjs/server";
4+
import {auth} from "@clerk/nextjs/server";
55

66
export async function updatePasswordItem(
77
id: string,
88
newUsername: string,
99
newWebsite: string,
10-
newPassword: string
10+
newPassword: string,
11+
usernameIV: string,
12+
websiteIV: string,
13+
passwordIV: string
1114
) {
1215
const { userId } = await auth()
1316

@@ -42,6 +45,9 @@ export async function updatePasswordItem(
4245
username: newUsername,
4346
website: newWebsite,
4447
password: newPassword,
48+
usernameIV,
49+
websiteIV,
50+
passwordIV,
4551
},
4652
});
4753

@@ -86,7 +92,10 @@ export async function deletePasswordItem(id: string) {
8692
export async function createPasswordItem(
8793
username: string,
8894
website: string,
89-
password: string
95+
password: string,
96+
usernameIV: string,
97+
websiteIV: string,
98+
passwordIV: string
9099
) {
91100
const { userId } = await auth()
92101

@@ -99,6 +108,9 @@ export async function createPasswordItem(
99108
username,
100109
website,
101110
password,
111+
usernameIV,
112+
websiteIV,
113+
passwordIV,
102114
updatedAt: new Date().toISOString(),
103115
createdAt: new Date().toISOString(),
104116
user: {

src/components/vault/dialogs/create-password-dialog.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ const initialPasswordItemState = {
2626

2727
export const CreatePasswordDialog = ({
2828
open,
29-
onClose,
29+
onClose,
3030
}: {
3131
open: boolean;
32-
onClose: ()=> void
32+
onClose: () => void;
3333
}) => {
3434
const [passwordItem, setPasswordItem] = useState(initialPasswordItemState);
3535
const [loading, setLoading] = useState(false);
@@ -68,12 +68,35 @@ export const CreatePasswordDialog = ({
6868
return;
6969
}
7070

71+
if (!clerkuser) {
72+
toast.error("User not found");
73+
setLoading(false);
74+
return;
75+
}
76+
7177
try {
78+
const encryptedUsername = await encrypt(
79+
passwordItem.username,
80+
clerkuser.id
81+
);
82+
const encryptedWebsite = await encrypt(
83+
passwordItem.website,
84+
clerkuser.id
85+
);
86+
const encryptedPassword = await encrypt(
87+
passwordItem.password,
88+
clerkuser.id
89+
);
90+
7291
await createPasswordItem(
73-
encrypt(passwordItem.username, clerkuser),
74-
encrypt(passwordItem.website, clerkuser),
75-
encrypt(passwordItem.password, clerkuser)
92+
encryptedUsername.encryptedData,
93+
encryptedWebsite.encryptedData,
94+
encryptedPassword.encryptedData,
95+
encryptedUsername.iv,
96+
encryptedWebsite.iv,
97+
encryptedPassword.iv
7698
);
99+
77100
toast.success("Password created");
78101
setPasswordItem(initialPasswordItemState);
79102
onClose();

src/components/vault/dialogs/edit-password-dialog.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,30 +78,45 @@ export function EditPasswordDialog({
7878
setEditedEntry((prev) => ({ ...prev, [name]: value }));
7979
};
8080

81-
const handleSave = () => {
81+
const handleSave = async () => {
8282
setLoading(true);
83+
8384
const validationResult = passwordSchema.safeParse({
8485
name: editedEntry.name,
8586
username: editedEntry.username,
8687
website: editedEntry.website,
8788
password: editedEntry.password,
8889
});
89-
90+
9091
if (!validationResult.success) {
9192
const errorMessage =
9293
validationResult.error.errors[0]?.message || "Validation failed";
9394
toast.error(errorMessage);
9495
setLoading(false);
9596
return;
9697
}
97-
98+
99+
if (!user) {
100+
toast.error("User not found");
101+
setLoading(false);
102+
return;
103+
}
104+
98105
try {
106+
const encryptedUsername = await encrypt(editedEntry.username, user.id);
107+
const encryptedWebsite = await encrypt(editedEntry.website, user.id);
108+
const encryptedPassword = await encrypt(editedEntry.password, user.id);
109+
99110
updatePasswordItem(
100111
entry!.id,
101-
encrypt(editedEntry.username, user),
102-
encrypt(editedEntry.website, user),
103-
encrypt(editedEntry.password, user)
112+
encryptedUsername.encryptedData,
113+
encryptedWebsite.encryptedData,
114+
encryptedPassword.encryptedData,
115+
encryptedUsername.iv,
116+
encryptedWebsite.iv,
117+
encryptedPassword.iv,
104118
);
119+
105120
router.refresh();
106121
toast.success("Password updated");
107122
onClose();
@@ -111,6 +126,7 @@ export function EditPasswordDialog({
111126
setLoading(false);
112127
}
113128
};
129+
114130

115131
return (
116132
<Dialog open={isOpen} onOpenChange={onClose}>
@@ -136,6 +152,7 @@ export function EditPasswordDialog({
136152
value={editedEntry.name}
137153
onChange={handleInputChange}
138154
maxLength={50}
155+
name="name"
139156
/>
140157
<div
141158
className="pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 text-xs tabular-nums text-muted-foreground peer-disabled:opacity-50"
@@ -151,6 +168,7 @@ export function EditPasswordDialog({
151168
value={editedEntry.username}
152169
onChange={handleInputChange}
153170
maxLength={30}
171+
name="username"
154172
/>
155173
<div
156174
className="pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 text-xs tabular-nums text-muted-foreground peer-disabled:opacity-50"
@@ -166,6 +184,7 @@ export function EditPasswordDialog({
166184
value={editedEntry.website}
167185
onChange={handleInputChange}
168186
maxLength={50}
187+
name="website"
169188
/>
170189
<div
171190
className="pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 text-xs tabular-nums text-muted-foreground peer-disabled:opacity-50"
@@ -182,6 +201,7 @@ export function EditPasswordDialog({
182201
onChange={handleInputChange}
183202
type="password"
184203
maxLength={128}
204+
name="password"
185205
/>
186206
<div
187207
className="pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 text-xs tabular-nums text-muted-foreground peer-disabled:opacity-50"

src/components/vault/vault-page.tsx

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
"use client";
22

3-
import {deletePasswordItem, getPasswords} from "@/app/actions";
4-
import {Button} from "@/components/ui/button";
5-
import {Input} from "@/components/ui/input";
6-
import {ScrollArea} from "@/components/ui/scroll-area";
7-
import {Sheet,SheetContent} from "@/components/ui/sheet";
8-
import {CreatePasswordDialog} from "@/components/vault/dialogs/create-password-dialog";
9-
import {EditPasswordDialog} from "@/components/vault/dialogs/edit-password-dialog";
10-
import {cn} from "@/lib/utils";
11-
import {decrypt} from "@/utils/encryption";
12-
import {useUser} from "@clerk/nextjs";
13-
import {Prisma} from "@prisma/client";
14-
import {Plus,SquareArrowOutUpRight,Trash,User} from "lucide-react";
3+
import { deletePasswordItem, getPasswords } from "@/app/actions";
4+
import { Button } from "@/components/ui/button";
5+
import { Input } from "@/components/ui/input";
6+
import { ScrollArea } from "@/components/ui/scroll-area";
7+
import { Sheet, SheetContent } from "@/components/ui/sheet";
8+
import { CreatePasswordDialog } from "@/components/vault/dialogs/create-password-dialog";
9+
import { EditPasswordDialog } from "@/components/vault/dialogs/edit-password-dialog";
10+
import { cn } from "@/lib/utils";
11+
import { decrypt, generateAndStoreKey, retrieveKey } from "@/utils/encryption";
12+
import { useUser } from "@clerk/nextjs";
13+
import { Prisma } from "@prisma/client";
14+
import { Plus, SquareArrowOutUpRight, Trash, User } from "lucide-react";
1515
import Image from "next/image";
16-
import {useRouter} from "next/navigation";
17-
import {useEffect,useState} from "react";
16+
import { useRouter } from "next/navigation";
17+
import { useEffect, useState } from "react";
1818
import toast from "react-hot-toast";
1919
import {
2020
ContextMenu,
@@ -23,16 +23,17 @@ import {
2323
ContextMenuLabel,
2424
ContextMenuTrigger,
2525
} from "../ui/context-menu";
26-
import {EmptyState} from "./empty-state";
27-
import {PasswordDetails} from "./password-details";
28-
import {Sidebar} from "./sidebar";
26+
import { EmptyState } from "./empty-state";
27+
import { PasswordDetails } from "./password-details";
28+
import { Sidebar } from "./sidebar";
2929

3030
interface PasswordEntry {
3131
id: string;
3232
name: string;
3333
username: string;
3434
website: string;
3535
password: string;
36+
iv: string;
3637
updatedAt: string;
3738
lastAccess: string;
3839
created: string;
@@ -63,30 +64,61 @@ export const VaultPage: React.FC<VaultPageProps> = ({ user }) => {
6364
const [searchQuery, setSearchQuery] = useState("");
6465
const [filteredEntries, setFilteredEntries] = useState<PasswordEntry[]>([]);
6566
const [passwords, setPasswords] = useState<PasswordEntry[]>([]);
66-
const [passwordItems, setPasswordItems] = useState(user?.passwordItems)
67+
const [passwordItems, setPasswordItems] = useState(user?.passwordItems);
6768

6869
useEffect(() => {
69-
if (!clerkUser) return;
70+
const ensureEncryptionKey = async () => {
71+
if (!clerkUser) return;
7072

71-
if (!user?.passwordItems || !passwordItems) return;
73+
const userId = clerkUser.id;
7274

73-
const decryptedPasswords = passwordItems
74-
.map((item) => ({
75-
id: item.id,
76-
name: decrypt(item.username, clerkUser),
77-
username: decrypt(item.username, clerkUser),
78-
website: decrypt(item.website, clerkUser),
79-
password: decrypt(item.password, clerkUser),
80-
updatedAt: item.updatedAt.toISOString(),
81-
lastAccess: item.updatedAt.toISOString(),
82-
created: item.createdAt.toISOString(),
83-
}))
84-
.sort(
85-
(a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()
86-
);
75+
try {
76+
await retrieveKey(userId);
77+
toast.success("Encryption key found");
78+
} catch {
79+
toast.success("Generating encryption key...");
80+
await generateAndStoreKey(userId);
81+
}
82+
};
83+
84+
ensureEncryptionKey();
85+
}, [clerkUser]);
8786

88-
setPasswords(decryptedPasswords);
87+
useEffect(() => {
88+
if (!clerkUser) return;
89+
90+
if (!user?.passwordItems || !passwordItems) return;
91+
92+
const decryptPasswords = async () => {
93+
const decryptedPasswords = await Promise.all(
94+
passwordItems.map(async (item) => {
95+
try {
96+
const decryptedItem = {
97+
id: item.id,
98+
name: await decrypt(item.username, item.usernameIV, clerkUser.id),
99+
username: await decrypt(item.username, item.usernameIV, clerkUser.id),
100+
website: await decrypt(item.website, item.websiteIV, clerkUser.id),
101+
password: await decrypt(item.password, item.passwordIV, clerkUser.id),
102+
updatedAt: item.updatedAt.toISOString(),
103+
lastAccess: item.updatedAt.toISOString(),
104+
created: item.createdAt.toISOString(),
105+
};
106+
return decryptedItem;
107+
} catch (error) {
108+
console.error(`Error decrypting item ID: ${item.id}`, error);
109+
return null;
110+
}
111+
})
112+
);
113+
114+
setPasswords(
115+
decryptedPasswords.filter((item): item is PasswordEntry => item !== null)
116+
);
117+
};
118+
119+
decryptPasswords();
89120
}, [user?.passwordItems, clerkUser, passwordItems]);
121+
90122

91123
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
92124
setSearchQuery(e.target.value);
@@ -212,8 +244,10 @@ export const VaultPage: React.FC<VaultPageProps> = ({ user }) => {
212244
onClick={async () => {
213245
try {
214246
await deletePasswordItem(password.id);
215-
const updatedItems = await getPasswords(user?.id as string)
216-
setPasswordItems(updatedItems?.passwordItems);
247+
const updatedItems = await getPasswords(
248+
user?.id as string
249+
);
250+
setPasswordItems(updatedItems?.passwordItems);
217251
if (selectedEntry?.id === password.id) {
218252
setSelectedEntry(null);
219253
}
@@ -285,8 +319,8 @@ export const VaultPage: React.FC<VaultPageProps> = ({ user }) => {
285319
onClose={async () => {
286320
setIsCreateDialogOpen(false);
287321
setSelectedEntry(null);
288-
const userWithPasswords = await getPasswords(user?.id as string)
289-
setPasswordItems(userWithPasswords?.passwordItems)
322+
const userWithPasswords = await getPasswords(user?.id as string);
323+
setPasswordItems(userWithPasswords?.passwordItems);
290324
}}
291325
/>
292326
</div>

0 commit comments

Comments
 (0)