mirror of
https://github.com/zadam/trilium.git
synced 2025-12-22 16:20:08 +01:00
Compare commits
10 Commits
fix/implem
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d32e1f0d8 | ||
|
|
d0f91e7709 | ||
|
|
353d626d45 | ||
|
|
af67a3ba11 | ||
|
|
a867c646e4 | ||
|
|
150e2504b1 | ||
|
|
aa7ae150dc | ||
|
|
d99e08bfdd | ||
|
|
29d038c76b | ||
|
|
f1615bb4f6 |
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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." }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user