Compare commits

..

10 Commits

Author SHA1 Message Date
Adorian Doran
0d32e1f0d8 style/classic toolbar: fix broken border radius 2025-12-22 02:43:39 +02:00
Adorian Doran
d0f91e7709 style/status bar: hide the focus outline for dropdown buttons 2025-12-22 02:37:00 +02:00
Adorian Doran
353d626d45 style/breadcrumb: tweak arrows 2025-12-22 02:29:05 +02:00
Adorian Doran
af67a3ba11 style/breadcrumb: use scrollable dropdowns for note listings 2025-12-22 02:24:34 +02:00
Adorian Doran
a867c646e4 style: refactor 2025-12-22 02:23:43 +02:00
Adorian Doran
150e2504b1 style: add (limited) support for scrollable menus 2025-12-22 02:20:56 +02:00
Adorian Doran
aa7ae150dc style/text editor/links: tweak 2025-12-22 02:04:35 +02:00
Adorian Doran
d99e08bfdd style/text editor: fix links 2025-12-22 01:39:33 +02:00
Elian Doran
29d038c76b Translations update from Hosted Weblate (#8130) 2025-12-22 00:25:39 +02:00
Barszczun
f1615bb4f6 Translated using Weblate (Polish)
Currently translated at 99.6% (1712 of 1718 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2025-12-21 23:11:12 +01:00
16 changed files with 58 additions and 77 deletions

View File

@@ -128,6 +128,12 @@ body.backdrop-effects-disabled {
font-size: 0.9rem !important; font-size: 0.9rem !important;
} }
.dropdown-menu.tn-dropdown-menu-scrollable {
/* Note: scrollable dropdowns does not support submenus */
max-height: 90vh;
overflow: scroll;
}
body.desktop .dropdown-menu::before, body.desktop .dropdown-menu::before,
:root .ck.ck-dropdown__panel::before, :root .ck.ck-dropdown__panel::before,
:root .excalidraw .popover::before, :root .excalidraw .popover::before,

View File

@@ -653,7 +653,8 @@ body a.tn-link:focus-visible,
} }
body a.tn-link:hover, body a.tn-link:hover,
.use-tn-links a:hover { .use-tn-links a:hover,
.use-tn-links a.ck-widget_selected {
box-shadow: 0 0 0 4px var(--link-hover-background); box-shadow: 0 0 0 4px var(--link-hover-background);
--background: var(--link-hover-background); --background: var(--link-hover-background);
color: var(--link-hover-color); color: var(--link-hover-color);

View File

@@ -670,6 +670,19 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
color: var(--main-text-color); color: var(--main-text-color);
} }
/* Links */
.ck-content a.ck-widget {
outline: none;
}
.ck-content a.ck-widget.ck-widget_selected,
.ck-content a.ck-link_selected {
outline: 2px solid var(--input-focus-outline-color);
outline-offset: 2px;
background: var(--link-hover-background);
}
/* Reference link */ /* Reference link */
.ck-content a.reference-link, .ck-content a.reference-link,
@@ -680,6 +693,10 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
} }
.ck-content a.reference-link > span { .ck-content a.reference-link > span {
color: var(--custom-color, inherit);
}
.ck-content a.reference-link:hover > span {
text-decoration: underline; text-decoration: underline;
} }

View File

@@ -1230,7 +1230,7 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
margin-bottom: 2px; margin-bottom: 2px;
} }
body.vertical-layout #rest-pane > .classic-toolbar-widget { body.layout-vertical #rest-pane > .classic-toolbar-widget {
border-start-start-radius: var(--center-pane-border-radius); border-start-start-radius: var(--center-pane-border-radius);
} }

View File

@@ -1934,7 +1934,10 @@
}, },
"highlights_list_2": { "highlights_list_2": {
"title": "Lista wyróżnień", "title": "Lista wyróżnień",
"options": "Opcje" "options": "Opcje",
"modal_title": "Konfiguracja listy wyróżnień",
"menu_configure": "Konfiguracja listy wyróżnień...",
"no_highlights": "Nie znaleziono wyróżnień."
}, },
"quick-search": { "quick-search": {
"placeholder": "Szybkie wyszukiwanie", "placeholder": "Szybkie wyszukiwanie",

View File

@@ -189,10 +189,11 @@ interface BreadcrumbSeparatorProps {
function BreadcrumbSeparator(props: BreadcrumbSeparatorProps) { function BreadcrumbSeparator(props: BreadcrumbSeparatorProps) {
return ( return (
<Dropdown <Dropdown
text={<Icon icon="bx bx-chevron-right" />} text={<Icon icon="bx bxs-chevron-right" />}
noSelectButtonStyle noSelectButtonStyle
buttonClassName="icon-action" buttonClassName="icon-action"
hideToggleArrow hideToggleArrow
dropdownContainerClassName="tn-dropdown-menu-scrollable"
dropdownOptions={{ popperConfig: { strategy: "fixed", placement: "top" } }} dropdownOptions={{ popperConfig: { strategy: "fixed", placement: "top" } }}
> >
<BreadcrumbSeparatorDropdownContent {...props} /> <BreadcrumbSeparatorDropdownContent {...props} />

View File

@@ -155,11 +155,6 @@
} }
} }
.dropdown-code-note-switcher {
max-height: 90vh;
overflow: scroll;
}
.backlinks-widget > .dropdown-menu { .backlinks-widget > .dropdown-menu {
--menu-padding-size: .9em; --menu-padding-size: .9em;
@@ -260,4 +255,8 @@
} }
} }
button.select-button:not(:focus-visible) {
outline: none;
}
} }

View File

@@ -422,7 +422,7 @@ function CodeNoteSwitcher({ note }: StatusBarContext) {
icon="bx bx-code-curly" icon="bx bx-code-curly"
text={correspondingMimeType?.title} text={correspondingMimeType?.title}
title={t("status_bar.code_note_switcher")} title={t("status_bar.code_note_switcher")}
dropdownContainerClassName="dropdown-code-note-switcher" dropdownContainerClassName="dropdown-code-note-switcher tn-dropdown-menu-scrollable"
> >
<NoteTypeCodeNoteList <NoteTypeCodeNoteList
currentMimeType={currentNoteMime} currentMimeType={currentNoteMime}

View File

@@ -108,7 +108,7 @@ function loginSync(req: Request) {
const givenHash = req.body.hash; const givenHash = req.body.hash;
if (!utils.constantTimeCompare(expectedHash, givenHash)) { if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect. It looks like you're trying to sync two different initialized documents which is not possible." }]; return [400, { message: "Sync login credentials are incorrect. It looks like you're trying to sync two different initialized documents which is not possible." }];
} }

View File

@@ -1,4 +1,3 @@
import crypto from "crypto";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import optionService from "../services/options.js"; import optionService from "../services/options.js";
import myScryptService from "../services/encryption/my_scrypt.js"; import myScryptService from "../services/encryption/my_scrypt.js";
@@ -161,11 +160,7 @@ function verifyPassword(submittedPassword: string) {
const guess_hashed = myScryptService.getVerificationHash(submittedPassword); const guess_hashed = myScryptService.getVerificationHash(submittedPassword);
// Use constant-time comparison to prevent timing attacks return guess_hashed.equals(hashed_password);
if (hashed_password.length !== guess_hashed.length) {
return false;
}
return crypto.timingSafeEqual(guess_hashed, hashed_password);
} }
function sendLoginError(req: Request, res: Response, errorType: 'password' | 'totp' = 'password') { function sendLoginError(req: Request, res: Response, errorType: 'password' | 'totp' = 'password') {

View File

@@ -1,5 +1,5 @@
import myScryptService from "./my_scrypt.js"; import myScryptService from "./my_scrypt.js";
import utils, { constantTimeCompare } from "../utils.js"; import utils from "../utils.js";
import dataEncryptionService from "./data_encryption.js"; import dataEncryptionService from "./data_encryption.js";
import sql from "../sql.js"; import sql from "../sql.js";
import sqlInit from "../sql_init.js"; import sqlInit from "../sql_init.js";
@@ -87,7 +87,8 @@ function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
return undefined; return undefined;
} }
return constantTimeCompare(givenHash, savedHash as string); console.log("Matches: " + givenHash === savedHash);
return givenHash === savedHash;
} }
function setDataKey( function setDataKey(

View File

@@ -1,6 +1,6 @@
import optionService from "../options.js"; import optionService from "../options.js";
import myScryptService from "./my_scrypt.js"; import myScryptService from "./my_scrypt.js";
import { toBase64, constantTimeCompare } from "../utils.js"; import { toBase64 } from "../utils.js";
import dataEncryptionService from "./data_encryption.js"; import dataEncryptionService from "./data_encryption.js";
function verifyPassword(password: string) { function verifyPassword(password: string) {
@@ -12,7 +12,7 @@ function verifyPassword(password: string) {
return false; return false;
} }
return constantTimeCompare(givenPasswordHash, dbPasswordHash); return givenPasswordHash === dbPasswordHash;
} }
function setDataKey(password: string, plainTextDataKey: string | Buffer) { function setDataKey(password: string, plainTextDataKey: string | Buffer) {

View File

@@ -1,7 +1,6 @@
import crypto from 'crypto'; import crypto from 'crypto';
import optionService from '../options.js'; import optionService from '../options.js';
import sql from '../sql.js'; import sql from '../sql.js';
import { constantTimeCompare } from '../utils.js';
function isRecoveryCodeSet() { function isRecoveryCodeSet() {
return optionService.getOptionBool('encryptedRecoveryCodes'); return optionService.getOptionBool('encryptedRecoveryCodes');
@@ -56,22 +55,13 @@ function verifyRecoveryCode(recoveryCodeGuess: string) {
const recoveryCodes = getRecoveryCodes(); const recoveryCodes = getRecoveryCodes();
let loginSuccess = false; let loginSuccess = false;
let matchedCode: string | null = null; recoveryCodes.forEach((recoveryCode) => {
if (recoveryCodeGuess === recoveryCode) {
// Check ALL codes to prevent timing attacks - do not short-circuit removeRecoveryCode(recoveryCode);
for (const recoveryCode of recoveryCodes) {
if (constantTimeCompare(recoveryCodeGuess, recoveryCode)) {
matchedCode = recoveryCode;
loginSuccess = true; loginSuccess = true;
// Continue checking all codes to maintain constant time return;
} }
} });
// Remove the matched code only after checking all codes
if (matchedCode) {
removeRecoveryCode(matchedCode);
}
return loginSuccess; return loginSuccess;
} }

View File

@@ -1,6 +1,6 @@
import optionService from "../options.js"; import optionService from "../options.js";
import myScryptService from "./my_scrypt.js"; import myScryptService from "./my_scrypt.js";
import { randomSecureToken, toBase64, constantTimeCompare } from "../utils.js"; import { randomSecureToken, toBase64 } from "../utils.js";
import dataEncryptionService from "./data_encryption.js"; import dataEncryptionService from "./data_encryption.js";
import type { OptionNames } from "@triliumnext/commons"; import type { OptionNames } from "@triliumnext/commons";
@@ -18,7 +18,7 @@ function verifyTotpSecret(secret: string): boolean {
return false; return false;
} }
return constantTimeCompare(givenSecretHash, dbSecretHash); return givenSecretHash === dbSecretHash;
} }
function setTotpSecret(secret: string) { function setTotpSecret(secret: string) {

View File

@@ -1,5 +1,5 @@
import becca from "../becca/becca.js"; import becca from "../becca/becca.js";
import { fromBase64, randomSecureToken, constantTimeCompare } from "./utils.js"; import { fromBase64, randomSecureToken } from "./utils.js";
import BEtapiToken from "../becca/entities/betapi_token.js"; import BEtapiToken from "../becca/entities/betapi_token.js";
import crypto from "crypto"; import crypto from "crypto";
@@ -83,16 +83,15 @@ function isValidAuthHeader(auth: string | undefined) {
return false; return false;
} }
return constantTimeCompare(etapiToken.tokenHash, authTokenHash); return etapiToken.tokenHash === authTokenHash;
} else { } else {
// Check ALL tokens to prevent timing attacks - do not short-circuit
let isValid = false;
for (const etapiToken of becca.getEtapiTokens()) { for (const etapiToken of becca.getEtapiTokens()) {
if (constantTimeCompare(etapiToken.tokenHash, authTokenHash)) { if (etapiToken.tokenHash === authTokenHash) {
isValid = true; return true;
} }
} }
return isValid;
return false;
} }
} }

View File

@@ -74,36 +74,6 @@ export function hmac(secret: any, value: any) {
return hmac.digest("base64"); return hmac.digest("base64");
} }
/**
* Constant-time string comparison to prevent timing attacks.
* Uses crypto.timingSafeEqual to ensure comparison time is independent
* of how many characters match.
*
* @param a First string to compare
* @param b Second string to compare
* @returns true if strings are equal, false otherwise
* @note Returns false for null/undefined/non-string inputs. Empty strings are considered equal.
*/
export function constantTimeCompare(a: string | null | undefined, b: string | null | undefined): boolean {
// Handle null/undefined/non-string cases safely
if (typeof a !== "string" || typeof b !== "string") {
return false;
}
const bufA = Buffer.from(a, "utf-8");
const bufB = Buffer.from(b, "utf-8");
// If lengths differ, we still do a constant-time comparison
// to avoid leaking length information through timing
if (bufA.length !== bufB.length) {
// Compare bufA against itself to maintain constant time behavior
crypto.timingSafeEqual(bufA, bufA);
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
}
export function hash(text: string) { export function hash(text: string) {
text = text.normalize(); text = text.normalize();
@@ -516,7 +486,6 @@ function slugify(text: string) {
export default { export default {
compareVersions, compareVersions,
constantTimeCompare,
crash, crash,
envToBoolean, envToBoolean,
escapeHtml, escapeHtml,