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