feat: enhance zero-knowledge authentication by deriving and sending auth hashes to the server

This commit is contained in:
ramvignesh-b
2026-04-14 22:44:42 +05:30
parent 3e5dbbe3f3
commit 967b3a77f8
10 changed files with 146 additions and 79 deletions
+20 -34
View File
@@ -1,6 +1,7 @@
import { act, renderHook } from "@testing-library/react";
import { HttpResponse, http } from "msw";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mockMasterKey } from "../../test/fixtures/auth.fixture";
import { mockUser } from "../../test/fixtures/user.fixture";
import { server } from "../../test/mocks/server";
import { useAuthStore } from "../store/useAuthStore";
@@ -13,26 +14,24 @@ import {
} from "../utils/keystore";
import { useAuth } from "./useAuth";
vi.mock("../utils/crypto");
vi.mock("../utils/keystore");
const API_URL = "http://piku-server";
vi.mock("../utils/crypto", () => ({
CryptoUtils: {
deriveMasterKey: vi
.fn()
.mockResolvedValue({ type: "secret" } as unknown as CryptoKey),
},
}));
vi.mock("../utils/keystore", () => ({
saveMasterKey: vi.fn().mockResolvedValue(undefined),
loadMasterKey: vi
.fn()
.mockResolvedValue({ type: "secret" } as unknown as CryptoKey),
clearMasterKey: vi.fn().mockResolvedValue(undefined),
}));
beforeEach(() => {
vi.clearAllMocks();
// hack to set up mock implementations using fixtures
vi.mocked(CryptoUtils.deriveKeyBundle).mockResolvedValue({
masterKey: mockMasterKey,
authHash: "mock-auth-hash",
});
vi.mocked(loadMasterKey).mockResolvedValue(mockMasterKey);
vi.mocked(saveMasterKey).mockResolvedValue(undefined);
vi.mocked(clearMasterKey).mockResolvedValue(undefined);
useAuthStore.setState({
accessToken: null,
user: null,
@@ -61,34 +60,21 @@ describe("isAuthenticated", () => {
});
describe("login", () => {
it("should derive the master key using the provided credentials", async () => {
it("should persist the provided master key to IndexedDB", async () => {
const { result } = renderHook(() => useAuth());
await act(async () => {
await result.current.login("access-token", mockUser, "test-password");
await result.current.login("access-token", mockUser, mockMasterKey);
});
expect(CryptoUtils.deriveMasterKey).toHaveBeenCalledWith(
"test-password",
mockUser.email,
);
});
it("should persist the derived master key to IndexedDB", async () => {
const { result } = renderHook(() => useAuth());
await act(async () => {
await result.current.login("access-token", mockUser, "my-password");
});
expect(saveMasterKey).toHaveBeenCalledTimes(1);
expect(saveMasterKey).toHaveBeenCalledWith(mockMasterKey);
});
it("should update the store with the access token and user profile", async () => {
const { result } = renderHook(() => useAuth());
await act(async () => {
await result.current.login("my-access-token", mockUser, "my-password");
await result.current.login("my-access-token", mockUser, mockMasterKey);
});
expect(useAuthStore.getState().accessToken).toBe("my-access-token");
@@ -99,7 +85,7 @@ describe("login", () => {
const { result } = renderHook(() => useAuth());
await act(async () => {
await result.current.login("token", mockUser, "my-password");
await result.current.login("token", mockUser, mockMasterKey);
});
expect(useKeyStore.getState().masterKey).not.toBeNull();
+4 -9
View File
@@ -4,7 +4,6 @@ import { endpoints } from "../config/endpoints";
import type { UserProfile } from "../store/useAuthStore";
import { useAuthStore } from "../store/useAuthStore";
import { useKeyStore } from "../store/useKeyStore";
import { CryptoUtils } from "../utils/crypto";
import {
clearMasterKey,
loadMasterKey,
@@ -18,16 +17,12 @@ export const useAuth = () => {
const isAuthenticated = !!accessToken;
// called after successful login — derive & save master key
const login = async (
// called after successful login — save master key
const setAuthStore = async (
access: string,
profile: UserProfile,
password: string,
masterKey: CryptoKey,
) => {
const masterKey = await CryptoUtils.deriveMasterKey(
password,
profile.email,
);
await saveMasterKey(masterKey);
setMasterKey(masterKey);
setAuth(access, profile);
@@ -76,7 +71,7 @@ export const useAuth = () => {
isAuthenticated,
user,
isInitializing,
login,
setAuthStore,
logout,
initialize,
};