Remove client-side SHA-256 pre-hashing from setup and login

The sha256Hex helper used window.crypto.subtle.digest(), which is only
available in a secure context (HTTPS / localhost). In the HTTP Docker
environment crypto.subtle is undefined, causing a TypeError before any
request is sent — the setup and login forms both silently failed with
'An unexpected error occurred'.

Fix: pass raw passwords directly to the API. The backend already applies
bcrypt, which is sufficient. No stored hashes need migration because
setup never completed successfully in the HTTP environment.

* frontend/src/pages/SetupPage.tsx  — remove sha256Hex call
* frontend/src/api/auth.ts          — remove sha256Hex call
* frontend/src/pages/__tests__/SetupPage.test.tsx — drop crypto mock
* frontend/src/utils/crypto.ts      — deleted (no remaining callers)
This commit is contained in:
2026-03-15 21:29:23 +01:00
parent cdf73e2d65
commit c41165c294
4 changed files with 3 additions and 41 deletions

View File

@@ -7,22 +7,16 @@
import { api } from "./client"; import { api } from "./client";
import { ENDPOINTS } from "./endpoints"; import { ENDPOINTS } from "./endpoints";
import type { LoginRequest, LoginResponse, LogoutResponse } from "../types/auth"; import type { LoginResponse, LogoutResponse } from "../types/auth";
import { sha256Hex } from "../utils/crypto";
/** /**
* Authenticate with the master password. * Authenticate with the master password.
* *
* The password is SHA-256 hashed client-side before transmission so that
* the plaintext never leaves the browser. The backend bcrypt-verifies the
* received hash against the stored bcrypt(sha256) digest.
*
* @param password - The master password entered by the user. * @param password - The master password entered by the user.
* @returns The login response containing the session token. * @returns The login response containing the session token.
*/ */
export async function login(password: string): Promise<LoginResponse> { export async function login(password: string): Promise<LoginResponse> {
const body: LoginRequest = { password: await sha256Hex(password) }; return api.post<LoginResponse>(ENDPOINTS.authLogin, { password });
return api.post<LoginResponse>(ENDPOINTS.authLogin, body);
} }
/** /**

View File

@@ -22,7 +22,6 @@ import { useNavigate } from "react-router-dom";
import type { ChangeEvent, FormEvent } from "react"; import type { ChangeEvent, FormEvent } from "react";
import { ApiError } from "../api/client"; import { ApiError } from "../api/client";
import { getSetupStatus, submitSetup } from "../api/setup"; import { getSetupStatus, submitSetup } from "../api/setup";
import { sha256Hex } from "../utils/crypto";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Styles // Styles
@@ -177,11 +176,8 @@ export function SetupPage(): React.JSX.Element {
setSubmitting(true); setSubmitting(true);
try { try {
// Hash the password client-side before transmission — the plaintext
// never leaves the browser. The backend bcrypt-hashes the received hash.
const hashedPassword = await sha256Hex(values.masterPassword);
await submitSetup({ await submitSetup({
master_password: hashedPassword, master_password: values.masterPassword,
database_path: values.databasePath, database_path: values.databasePath,
fail2ban_socket: values.fail2banSocket, fail2ban_socket: values.fail2banSocket,
timezone: values.timezone, timezone: values.timezone,

View File

@@ -10,11 +10,6 @@ vi.mock("../../api/setup", () => ({
submitSetup: vi.fn(), submitSetup: vi.fn(),
})); }));
// Mock the crypto utility — we only need it to resolve without testing SHA256.
vi.mock("../../utils/crypto", () => ({
sha256Hex: vi.fn().mockResolvedValue("hashed-password"),
}));
import { getSetupStatus } from "../../api/setup"; import { getSetupStatus } from "../../api/setup";
const mockedGetSetupStatus = vi.mocked(getSetupStatus); const mockedGetSetupStatus = vi.mocked(getSetupStatus);

View File

@@ -1,23 +0,0 @@
/**
* Client-side cryptography utilities.
*
* Uses the browser-native SubtleCrypto API so no third-party bundle is required.
*/
/**
* Return the SHA-256 hex digest of `input`.
*
* Hashing passwords before transmission means the plaintext never leaves the
* browser, even when HTTPS is not enforced in a development environment.
* The backend then applies bcrypt on top of the received hash.
*
* @param input - The string to hash (e.g. the master password).
* @returns Lowercase hex-encoded SHA-256 digest.
*/
export async function sha256Hex(input: string): Promise<string> {
const data = new TextEncoder().encode(input);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}