main f6d8405

password.ts RawHistoryFolder

1/* Removed Node's crypto import. Using Web Crypto API instead */
2
3/**
4 * Helper function that converts an ArrayBuffer to a hex string.
5 */
6function bufferToHex(buffer: ArrayBuffer): string {
7	return Array.from(new Uint8Array(buffer))
8		.map((b) => b.toString(16).padStart(2, '0'))
9		.join('');
10}
11
12/**
13 * Helper function that converts a hex string to a Uint8Array.
14 */
15function hexToBuffer(hex: string): Uint8Array {
16	if (hex.length % 2 !== 0) {
17		throw new Error('Invalid hex string');
18	}
19	const arr = new Uint8Array(hex.length / 2);
20	for (let i = 0; i < hex.length; i += 2) {
21		arr[i / 2] = parseInt(hex.substring(i, i + 2), 16);
22	}
23	return arr;
24}
25
26/**
27 * Performs a constant-time string comparison to prevent timing attacks.
28 * Returns true if strings are equal, false otherwise.
29 */
30function constantTimeCompare(a: string, b: string): boolean {
31	if (a.length !== b.length) {
32		return false;
33	}
34	let result = 0;
35	for (let i = 0; i < a.length; i++) {
36		result |= a.charCodeAt(i) ^ b.charCodeAt(i);
37	}
38	return result === 0;
39}
40
41/**
42 * Derives a password hash using PBKDF2 with the provided salt.
43 * @param password The plain text password.
44 * @param salt Uint8Array salt.
45 * @returns The derived key in hex format.
46 */
47async function derivePasswordHash(password: string, salt: Uint8Array): Promise<string> {
48	const enc = new TextEncoder();
49	const keyMaterial = enc.encode(password);
50	const key = await crypto.subtle.importKey('raw', keyMaterial, { name: 'PBKDF2' }, false, [
51		'deriveBits'
52	]);
53	const derivedBits = await crypto.subtle.deriveBits(
54		{
55			name: 'PBKDF2',
56			salt: salt.buffer as ArrayBuffer,
57			iterations: 100000,
58			hash: 'SHA-512'
59		},
60		key,
61		512
62	);
63	return bufferToHex(derivedBits);
64}
65
66/**
67 * Securely hashes a password using PBKDF2 with a random salt.
68 * @param password The plain text password to hash.
69 * @returns A promise that resolves to the hashed password in the format 'salt:hash'.
70 */
71export async function hashPassword(password: string): Promise<string> {
72	const salt = crypto.getRandomValues(new Uint8Array(16));
73	const saltHex = bufferToHex(salt.buffer);
74	const hash = await derivePasswordHash(password, salt);
75	return `${saltHex}:${hash}`;
76}
77
78/**
79 * Verify a password hash created by hashPassword().
80 * @param password The plain text password to verify.
81 * @param hash The hashed password to verify against in format 'salt:hash'.
82 * @returns A promise that resolves to true if the password matches, false otherwise.
83 */
84export async function verifyPassword(password: string, hash: string): Promise<boolean> {
85	const [saltHex, hashValue] = hash.split(':');
86	const salt = hexToBuffer(saltHex);
87	const derivedHash = await derivePasswordHash(password, salt);
88	return constantTimeCompare(derivedHash, hashValue);
89}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close