mirror of
https://github.com/zadam/trilium.git
synced 2025-12-29 10:44:25 +01:00
feat(login): implement better time comparison management across several features
This commit is contained in:
parent
ea1efb785b
commit
409ecb84a8
@ -108,7 +108,7 @@ function loginSync(req: Request) {
|
||||
|
||||
const givenHash = req.body.hash;
|
||||
|
||||
if (expectedHash !== givenHash) {
|
||||
if (!utils.constantTimeCompare(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." }];
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import myScryptService from "./my_scrypt.js";
|
||||
import utils from "../utils.js";
|
||||
import utils, { constantTimeCompare } from "../utils.js";
|
||||
import dataEncryptionService from "./data_encryption.js";
|
||||
import sql from "../sql.js";
|
||||
import sqlInit from "../sql_init.js";
|
||||
@ -87,8 +87,7 @@ function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.log("Matches: " + givenHash === savedHash);
|
||||
return givenHash === savedHash;
|
||||
return constantTimeCompare(givenHash, savedHash as string);
|
||||
}
|
||||
|
||||
function setDataKey(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import optionService from "../options.js";
|
||||
import myScryptService from "./my_scrypt.js";
|
||||
import { toBase64 } from "../utils.js";
|
||||
import { toBase64, constantTimeCompare } from "../utils.js";
|
||||
import dataEncryptionService from "./data_encryption.js";
|
||||
|
||||
function verifyPassword(password: string) {
|
||||
@ -12,7 +12,7 @@ function verifyPassword(password: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return givenPasswordHash === dbPasswordHash;
|
||||
return constantTimeCompare(givenPasswordHash, dbPasswordHash);
|
||||
}
|
||||
|
||||
function setDataKey(password: string, plainTextDataKey: string | Buffer) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import crypto from 'crypto';
|
||||
import optionService from '../options.js';
|
||||
import sql from '../sql.js';
|
||||
import { constantTimeCompare } from '../utils.js';
|
||||
|
||||
function isRecoveryCodeSet() {
|
||||
return optionService.getOptionBool('encryptedRecoveryCodes');
|
||||
@ -56,7 +57,7 @@ function verifyRecoveryCode(recoveryCodeGuess: string) {
|
||||
const recoveryCodes = getRecoveryCodes();
|
||||
let loginSuccess = false;
|
||||
recoveryCodes.forEach((recoveryCode) => {
|
||||
if (recoveryCodeGuess === recoveryCode) {
|
||||
if (constantTimeCompare(recoveryCodeGuess, recoveryCode)) {
|
||||
removeRecoveryCode(recoveryCode);
|
||||
loginSuccess = true;
|
||||
return;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import optionService from "../options.js";
|
||||
import myScryptService from "./my_scrypt.js";
|
||||
import { randomSecureToken, toBase64 } from "../utils.js";
|
||||
import { randomSecureToken, toBase64, constantTimeCompare } from "../utils.js";
|
||||
import dataEncryptionService from "./data_encryption.js";
|
||||
import type { OptionNames } from "@triliumnext/commons";
|
||||
|
||||
@ -18,7 +18,7 @@ function verifyTotpSecret(secret: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
return givenSecretHash === dbSecretHash;
|
||||
return constantTimeCompare(givenSecretHash, dbSecretHash);
|
||||
}
|
||||
|
||||
function setTotpSecret(secret: string) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import becca from "../becca/becca.js";
|
||||
import { fromBase64, randomSecureToken } from "./utils.js";
|
||||
import { fromBase64, randomSecureToken, constantTimeCompare } from "./utils.js";
|
||||
import BEtapiToken from "../becca/entities/betapi_token.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
@ -83,10 +83,10 @@ function isValidAuthHeader(auth: string | undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return etapiToken.tokenHash === authTokenHash;
|
||||
return constantTimeCompare(etapiToken.tokenHash, authTokenHash);
|
||||
} else {
|
||||
for (const etapiToken of becca.getEtapiTokens()) {
|
||||
if (etapiToken.tokenHash === authTokenHash) {
|
||||
if (constantTimeCompare(etapiToken.tokenHash, authTokenHash)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,6 +74,30 @@ export function hmac(secret: any, value: any) {
|
||||
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
|
||||
*/
|
||||
export function constantTimeCompare(a: string, b: string): boolean {
|
||||
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) {
|
||||
text = text.normalize();
|
||||
|
||||
@ -486,6 +510,7 @@ function slugify(text: string) {
|
||||
|
||||
export default {
|
||||
compareVersions,
|
||||
constantTimeCompare,
|
||||
crash,
|
||||
envToBoolean,
|
||||
escapeHtml,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user