mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 11:56:01 +01:00 
			
		
		
		
	chore(nx): move all monorepo-style in subfolder for processing
This commit is contained in:
		@@ -1,114 +0,0 @@
 | 
			
		||||
import crypto from "crypto";
 | 
			
		||||
import log from "../log.js";
 | 
			
		||||
 | 
			
		||||
function arraysIdentical(a: any[] | Buffer, b: any[] | Buffer) {
 | 
			
		||||
    let i = a.length;
 | 
			
		||||
    if (i !== b.length) return false;
 | 
			
		||||
    while (i--) {
 | 
			
		||||
        if (a[i] !== b[i]) return false;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function shaArray(content: crypto.BinaryLike) {
 | 
			
		||||
    // we use this as a simple checksum and don't rely on its security, so SHA-1 is good enough
 | 
			
		||||
    return crypto.createHash("sha1").update(content).digest();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function pad(data: Buffer): Buffer {
 | 
			
		||||
    if (data.length > 16) {
 | 
			
		||||
        data = data.slice(0, 16);
 | 
			
		||||
    } else if (data.length < 16) {
 | 
			
		||||
        const zeros = Array(16 - data.length).fill(0);
 | 
			
		||||
 | 
			
		||||
        data = Buffer.concat([data, Buffer.from(zeros)]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Buffer.from(data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function encrypt(key: Buffer, plainText: Buffer | string) {
 | 
			
		||||
    if (!key) {
 | 
			
		||||
        throw new Error("No data key!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const plainTextBuffer = Buffer.isBuffer(plainText) ? plainText : Buffer.from(plainText);
 | 
			
		||||
 | 
			
		||||
    const iv = crypto.randomBytes(16);
 | 
			
		||||
    const cipher = crypto.createCipheriv("aes-128-cbc", pad(key), pad(iv));
 | 
			
		||||
 | 
			
		||||
    const digest = shaArray(plainTextBuffer).slice(0, 4);
 | 
			
		||||
 | 
			
		||||
    const digestWithPayload = Buffer.concat([digest, plainTextBuffer]);
 | 
			
		||||
 | 
			
		||||
    const encryptedData = Buffer.concat([cipher.update(digestWithPayload), cipher.final()]);
 | 
			
		||||
 | 
			
		||||
    const encryptedDataWithIv = Buffer.concat([iv, encryptedData]);
 | 
			
		||||
 | 
			
		||||
    return encryptedDataWithIv.toString("base64");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | null {
 | 
			
		||||
    if (cipherText === null) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!key) {
 | 
			
		||||
        return Buffer.from("[protected]");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
 | 
			
		||||
 | 
			
		||||
        // old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
 | 
			
		||||
        const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
 | 
			
		||||
 | 
			
		||||
        const iv = cipherTextBufferWithIv.slice(0, ivLength);
 | 
			
		||||
 | 
			
		||||
        const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
 | 
			
		||||
 | 
			
		||||
        const decipher = crypto.createDecipheriv("aes-128-cbc", pad(key), pad(iv));
 | 
			
		||||
 | 
			
		||||
        const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]);
 | 
			
		||||
 | 
			
		||||
        const digest = decryptedBytes.slice(0, 4);
 | 
			
		||||
        const payload = decryptedBytes.slice(4);
 | 
			
		||||
 | 
			
		||||
        const computedDigest = shaArray(payload).slice(0, 4);
 | 
			
		||||
 | 
			
		||||
        if (!arraysIdentical(digest, computedDigest)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return payload;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
        // recovery from https://github.com/zadam/trilium/issues/510
 | 
			
		||||
        if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) {
 | 
			
		||||
            log.info("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
 | 
			
		||||
 | 
			
		||||
            return (Buffer.isBuffer(cipherText) ? cipherText : Buffer.from(cipherText));
 | 
			
		||||
        } else {
 | 
			
		||||
            throw e;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function decryptString(dataKey: Buffer, cipherText: string) {
 | 
			
		||||
    const buffer = decrypt(dataKey, cipherText);
 | 
			
		||||
 | 
			
		||||
    if (buffer === null) {
 | 
			
		||||
        return null;
 | 
			
		||||
    } else if (buffer === false) {
 | 
			
		||||
        log.error(`Could not decrypt string. Buffer: ${buffer}`);
 | 
			
		||||
 | 
			
		||||
        throw new Error("Could not decrypt string.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return buffer.toString("utf-8");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    encrypt,
 | 
			
		||||
    decrypt,
 | 
			
		||||
    decryptString
 | 
			
		||||
};
 | 
			
		||||
@@ -1,64 +0,0 @@
 | 
			
		||||
import optionService from "../options.js";
 | 
			
		||||
import crypto from "crypto";
 | 
			
		||||
import sql from "../sql.js";
 | 
			
		||||
 | 
			
		||||
function getVerificationHash(password: crypto.BinaryLike) {
 | 
			
		||||
    const salt = optionService.getOption("passwordVerificationSalt");
 | 
			
		||||
 | 
			
		||||
    return getScryptHash(password, salt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getPasswordDerivedKey(password: crypto.BinaryLike) {
 | 
			
		||||
    const salt = optionService.getOption("passwordDerivedKeySalt");
 | 
			
		||||
 | 
			
		||||
    return getScryptHash(password, salt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getScryptHash(password: crypto.BinaryLike, salt: crypto.BinaryLike) {
 | 
			
		||||
    const hashed = crypto.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
 | 
			
		||||
 | 
			
		||||
    return hashed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSubjectIdentifierVerificationHash(
 | 
			
		||||
    guessedUserId: string | crypto.BinaryLike,
 | 
			
		||||
    salt?: string
 | 
			
		||||
) {
 | 
			
		||||
    if (salt != null) return getScryptHash(guessedUserId, salt);
 | 
			
		||||
 | 
			
		||||
    const savedSalt = sql.getValue("SELECT salt FROM user_data;");
 | 
			
		||||
    if (!savedSalt) {
 | 
			
		||||
        console.error("User salt undefined!");
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
    return getScryptHash(guessedUserId, savedSalt.toString());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSubjectIdentifierDerivedKey(
 | 
			
		||||
    subjectIdentifer: crypto.BinaryLike,
 | 
			
		||||
    givenSalt?: string
 | 
			
		||||
) {
 | 
			
		||||
    if (givenSalt !== undefined) {
 | 
			
		||||
        return getScryptHash(subjectIdentifer, givenSalt.toString());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const salt = sql.getValue("SELECT salt FROM user_data;");
 | 
			
		||||
    if (!salt) return undefined;
 | 
			
		||||
 | 
			
		||||
    return getScryptHash(subjectIdentifer, salt.toString());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createSubjectIdentifierDerivedKey(
 | 
			
		||||
    subjectIdentifer: string | crypto.BinaryLike,
 | 
			
		||||
    salt: string | crypto.BinaryLike
 | 
			
		||||
) {
 | 
			
		||||
    return getScryptHash(subjectIdentifer, salt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    getVerificationHash,
 | 
			
		||||
    getPasswordDerivedKey,
 | 
			
		||||
    getSubjectIdentifierVerificationHash,
 | 
			
		||||
    getSubjectIdentifierDerivedKey,
 | 
			
		||||
    createSubjectIdentifierDerivedKey
 | 
			
		||||
};
 | 
			
		||||
@@ -1,145 +0,0 @@
 | 
			
		||||
import myScryptService from "./my_scrypt.js";
 | 
			
		||||
import utils from "../utils.js";
 | 
			
		||||
import dataEncryptionService from "./data_encryption.js";
 | 
			
		||||
import sql from "../sql.js";
 | 
			
		||||
import sqlInit from "../sql_init.js";
 | 
			
		||||
import OpenIdError from "../../errors/open_id_error.js";
 | 
			
		||||
 | 
			
		||||
function saveUser(subjectIdentifier: string, name: string, email: string) {
 | 
			
		||||
    if (isUserSaved()) return false;
 | 
			
		||||
 | 
			
		||||
    const verificationSalt = utils.randomSecureToken(32);
 | 
			
		||||
    const derivedKeySalt = utils.randomSecureToken(32);
 | 
			
		||||
 | 
			
		||||
    const verificationHash = myScryptService.getSubjectIdentifierVerificationHash(
 | 
			
		||||
        subjectIdentifier,
 | 
			
		||||
        verificationSalt
 | 
			
		||||
    );
 | 
			
		||||
    if (!verificationHash) {
 | 
			
		||||
        throw new OpenIdError("Verification hash undefined!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userIDEncryptedDataKey = setDataKey(
 | 
			
		||||
        subjectIdentifier,
 | 
			
		||||
        utils.randomSecureToken(16),
 | 
			
		||||
        verificationSalt
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!userIDEncryptedDataKey) {
 | 
			
		||||
        console.error("UserID encrypted data key null");
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const data = {
 | 
			
		||||
        tmpID: 0,
 | 
			
		||||
        userIDVerificationHash: utils.toBase64(verificationHash),
 | 
			
		||||
        salt: verificationSalt,
 | 
			
		||||
        derivedKey: derivedKeySalt,
 | 
			
		||||
        userIDEncryptedDataKey: userIDEncryptedDataKey,
 | 
			
		||||
        isSetup: "true",
 | 
			
		||||
        username: name,
 | 
			
		||||
        email: email
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    sql.upsert("user_data", "tmpID", data);
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isSubjectIdentifierSaved() {
 | 
			
		||||
    const value = sql.getValue("SELECT userIDEncryptedDataKey FROM user_data;");
 | 
			
		||||
    if (value === undefined || value === null || value === "") return false;
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isUserSaved() {
 | 
			
		||||
    const isSaved = sql.getValue<string>("SELECT isSetup FROM user_data;");
 | 
			
		||||
    return isSaved === "true" ? true : false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
 | 
			
		||||
    if (!sqlInit.isDbInitialized()) {
 | 
			
		||||
        throw new OpenIdError("Database not initialized!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isUserSaved()) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const salt = sql.getValue("SELECT salt FROM user_data;");
 | 
			
		||||
    if (salt == undefined) {
 | 
			
		||||
        console.log("Salt undefined");
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const givenHash = myScryptService
 | 
			
		||||
        .getSubjectIdentifierVerificationHash(subjectIdentifier)
 | 
			
		||||
        ?.toString("base64");
 | 
			
		||||
    if (givenHash === undefined) {
 | 
			
		||||
        console.log("Sub id hash undefined!");
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const savedHash = sql.getValue(
 | 
			
		||||
        "SELECT userIDVerificationHash FROM user_data"
 | 
			
		||||
    );
 | 
			
		||||
    if (savedHash === undefined) {
 | 
			
		||||
        console.log("verification hash undefined");
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log("Matches: " + givenHash === savedHash);
 | 
			
		||||
    return givenHash === savedHash;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setDataKey(
 | 
			
		||||
    subjectIdentifier: string,
 | 
			
		||||
    plainTextDataKey: string | Buffer,
 | 
			
		||||
    salt: string
 | 
			
		||||
) {
 | 
			
		||||
    const subjectIdentifierDerivedKey =
 | 
			
		||||
        myScryptService.getSubjectIdentifierDerivedKey(subjectIdentifier, salt);
 | 
			
		||||
 | 
			
		||||
    if (subjectIdentifierDerivedKey === undefined) {
 | 
			
		||||
        console.error("SOMETHING WENT WRONG SAVING USER ID DERIVED KEY");
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
    const newEncryptedDataKey = dataEncryptionService.encrypt(
 | 
			
		||||
        subjectIdentifierDerivedKey,
 | 
			
		||||
        plainTextDataKey
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return newEncryptedDataKey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDataKey(subjectIdentifier: string) {
 | 
			
		||||
    const subjectIdentifierDerivedKey =
 | 
			
		||||
        myScryptService.getSubjectIdentifierDerivedKey(subjectIdentifier);
 | 
			
		||||
 | 
			
		||||
    const encryptedDataKey = sql.getValue(
 | 
			
		||||
        "SELECT userIDEncryptedDataKey FROM user_data"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!encryptedDataKey) {
 | 
			
		||||
        console.error("Encrypted data key empty!");
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!subjectIdentifierDerivedKey) {
 | 
			
		||||
        console.error("SOMETHING WENT WRONG SAVING USER ID DERIVED KEY");
 | 
			
		||||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
    const decryptedDataKey = dataEncryptionService.decrypt(
 | 
			
		||||
        subjectIdentifierDerivedKey,
 | 
			
		||||
        encryptedDataKey.toString()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return decryptedDataKey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    verifyOpenIDSubjectIdentifier,
 | 
			
		||||
    getDataKey,
 | 
			
		||||
    setDataKey,
 | 
			
		||||
    saveUser,
 | 
			
		||||
    isSubjectIdentifierSaved,
 | 
			
		||||
};
 | 
			
		||||
@@ -1,84 +0,0 @@
 | 
			
		||||
import sql from "../sql.js";
 | 
			
		||||
import optionService from "../options.js";
 | 
			
		||||
import myScryptService from "./my_scrypt.js";
 | 
			
		||||
import { randomSecureToken, toBase64 } from "../utils.js";
 | 
			
		||||
import passwordEncryptionService from "./password_encryption.js";
 | 
			
		||||
 | 
			
		||||
function isPasswordSet() {
 | 
			
		||||
    return !!sql.getValue("SELECT value FROM options WHERE name = 'passwordVerificationHash'");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function changePassword(currentPassword: string, newPassword: string) {
 | 
			
		||||
    if (!isPasswordSet()) {
 | 
			
		||||
        throw new Error("Password has not been set yet, so it cannot be changed. Use 'setPassword' instead.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!passwordEncryptionService.verifyPassword(currentPassword)) {
 | 
			
		||||
        return {
 | 
			
		||||
            success: false,
 | 
			
		||||
            message: "Given current password doesn't match hash"
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sql.transactional(() => {
 | 
			
		||||
        const decryptedDataKey = passwordEncryptionService.getDataKey(currentPassword);
 | 
			
		||||
 | 
			
		||||
        optionService.setOption("passwordVerificationSalt", randomSecureToken(32));
 | 
			
		||||
        optionService.setOption("passwordDerivedKeySalt", randomSecureToken(32));
 | 
			
		||||
 | 
			
		||||
        const newPasswordVerificationKey = toBase64(myScryptService.getVerificationHash(newPassword));
 | 
			
		||||
 | 
			
		||||
        if (decryptedDataKey) {
 | 
			
		||||
            // TODO: what should happen if the decrypted data key is null?
 | 
			
		||||
            passwordEncryptionService.setDataKey(newPassword, decryptedDataKey);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        optionService.setOption("passwordVerificationHash", newPasswordVerificationKey);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        success: true
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setPassword(password: string) {
 | 
			
		||||
    if (isPasswordSet()) {
 | 
			
		||||
        throw new Error("Password is set already. Either change it or perform 'reset password' first.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    optionService.createOption("passwordVerificationSalt", randomSecureToken(32), true);
 | 
			
		||||
    optionService.createOption("passwordDerivedKeySalt", randomSecureToken(32), true);
 | 
			
		||||
 | 
			
		||||
    const passwordVerificationKey = toBase64(myScryptService.getVerificationHash(password));
 | 
			
		||||
    optionService.createOption("passwordVerificationHash", passwordVerificationKey, true);
 | 
			
		||||
 | 
			
		||||
    // passwordEncryptionService expects these options to already exist
 | 
			
		||||
    optionService.createOption("encryptedDataKey", "", true);
 | 
			
		||||
 | 
			
		||||
    passwordEncryptionService.setDataKey(password, randomSecureToken(16));
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        success: true
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resetPassword() {
 | 
			
		||||
    // user forgot the password,
 | 
			
		||||
    sql.transactional(() => {
 | 
			
		||||
        optionService.setOption("passwordVerificationSalt", "");
 | 
			
		||||
        optionService.setOption("passwordDerivedKeySalt", "");
 | 
			
		||||
        optionService.setOption("encryptedDataKey", "");
 | 
			
		||||
        optionService.setOption("passwordVerificationHash", "");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        success: true
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    isPasswordSet,
 | 
			
		||||
    changePassword,
 | 
			
		||||
    setPassword,
 | 
			
		||||
    resetPassword
 | 
			
		||||
};
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
import optionService from "../options.js";
 | 
			
		||||
import myScryptService from "./my_scrypt.js";
 | 
			
		||||
import { toBase64 } from "../utils.js";
 | 
			
		||||
import dataEncryptionService from "./data_encryption.js";
 | 
			
		||||
 | 
			
		||||
function verifyPassword(password: string) {
 | 
			
		||||
    const givenPasswordHash = toBase64(myScryptService.getVerificationHash(password));
 | 
			
		||||
 | 
			
		||||
    const dbPasswordHash = optionService.getOptionOrNull("passwordVerificationHash");
 | 
			
		||||
 | 
			
		||||
    if (!dbPasswordHash) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return givenPasswordHash === dbPasswordHash;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setDataKey(password: string, plainTextDataKey: string | Buffer) {
 | 
			
		||||
    const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
 | 
			
		||||
 | 
			
		||||
    const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey);
 | 
			
		||||
 | 
			
		||||
    optionService.setOption("encryptedDataKey", newEncryptedDataKey);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDataKey(password: string) {
 | 
			
		||||
    const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
 | 
			
		||||
 | 
			
		||||
    const encryptedDataKey = optionService.getOption("encryptedDataKey");
 | 
			
		||||
 | 
			
		||||
    const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey);
 | 
			
		||||
 | 
			
		||||
    return decryptedDataKey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    verifyPassword,
 | 
			
		||||
    getDataKey,
 | 
			
		||||
    setDataKey
 | 
			
		||||
};
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
import crypto from 'crypto';
 | 
			
		||||
import optionService from '../options.js';
 | 
			
		||||
import sql from '../sql.js';
 | 
			
		||||
 | 
			
		||||
function isRecoveryCodeSet() {
 | 
			
		||||
    return optionService.getOptionBool('encryptedRecoveryCodes');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setRecoveryCodes(recoveryCodes: string) {
 | 
			
		||||
    const iv = crypto.randomBytes(16);
 | 
			
		||||
    const securityKey = crypto.randomBytes(32);
 | 
			
		||||
    const cipher = crypto.createCipheriv('aes-256-cbc', securityKey, iv);
 | 
			
		||||
    const encryptedRecoveryCodes = cipher.update(recoveryCodes, 'utf-8', 'hex');
 | 
			
		||||
 | 
			
		||||
    sql.transactional(() => {
 | 
			
		||||
        optionService.setOption('recoveryCodeInitialVector', iv.toString('hex'));
 | 
			
		||||
        optionService.setOption('recoveryCodeSecurityKey', securityKey.toString('hex'));
 | 
			
		||||
        optionService.setOption('recoveryCodesEncrypted', encryptedRecoveryCodes + cipher.final('hex'));
 | 
			
		||||
        optionService.setOption('encryptedRecoveryCodes', 'true');
 | 
			
		||||
        return true;
 | 
			
		||||
    });
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRecoveryCodes() {
 | 
			
		||||
    if (!isRecoveryCodeSet()) {
 | 
			
		||||
        return []
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return sql.transactional<string[]>(() => {
 | 
			
		||||
        const iv = Buffer.from(optionService.getOption('recoveryCodeInitialVector'), 'hex');
 | 
			
		||||
        const securityKey = Buffer.from(optionService.getOption('recoveryCodeSecurityKey'), 'hex');
 | 
			
		||||
        const encryptedRecoveryCodes = optionService.getOption('recoveryCodesEncrypted');
 | 
			
		||||
 | 
			
		||||
        const decipher = crypto.createDecipheriv('aes-256-cbc', securityKey, iv);
 | 
			
		||||
        const decryptedData = decipher.update(encryptedRecoveryCodes, 'hex', 'utf-8');
 | 
			
		||||
 | 
			
		||||
        const decryptedString = decryptedData + decipher.final('utf-8');
 | 
			
		||||
        return decryptedString.split(',');
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeRecoveryCode(usedCode: string) {
 | 
			
		||||
    const oldCodes = getRecoveryCodes();
 | 
			
		||||
    const today = new Date();
 | 
			
		||||
    oldCodes[oldCodes.indexOf(usedCode)] = today.toJSON().replace(/-/g, '/');
 | 
			
		||||
    setRecoveryCodes(oldCodes.toString());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function verifyRecoveryCode(recoveryCodeGuess: string) {
 | 
			
		||||
    const recoveryCodeRegex = RegExp(/^.{22}==$/gm);
 | 
			
		||||
    if (!recoveryCodeRegex.test(recoveryCodeGuess)) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const recoveryCodes = getRecoveryCodes();
 | 
			
		||||
    let loginSuccess = false;
 | 
			
		||||
    recoveryCodes.forEach((recoveryCode) => {
 | 
			
		||||
        if (recoveryCodeGuess === recoveryCode) {
 | 
			
		||||
            removeRecoveryCode(recoveryCode);
 | 
			
		||||
            loginSuccess = true;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    return loginSuccess;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    setRecoveryCodes,
 | 
			
		||||
    getRecoveryCodes,
 | 
			
		||||
    verifyRecoveryCode,
 | 
			
		||||
    isRecoveryCodeSet
 | 
			
		||||
};
 | 
			
		||||
@@ -1,83 +0,0 @@
 | 
			
		||||
import optionService from "../options.js";
 | 
			
		||||
import myScryptService from "./my_scrypt.js";
 | 
			
		||||
import { randomSecureToken, toBase64 } from "../utils.js";
 | 
			
		||||
import dataEncryptionService from "./data_encryption.js";
 | 
			
		||||
import type { OptionNames } from "@triliumnext/commons";
 | 
			
		||||
 | 
			
		||||
const TOTP_OPTIONS: Record<string, OptionNames> = {
 | 
			
		||||
    SALT: "totpEncryptionSalt",
 | 
			
		||||
    ENCRYPTED_SECRET: "totpEncryptedSecret",
 | 
			
		||||
    VERIFICATION_HASH: "totpVerificationHash"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function verifyTotpSecret(secret: string): boolean {
 | 
			
		||||
    const givenSecretHash = toBase64(myScryptService.getVerificationHash(secret));
 | 
			
		||||
    const dbSecretHash = optionService.getOptionOrNull(TOTP_OPTIONS.VERIFICATION_HASH);
 | 
			
		||||
 | 
			
		||||
    if (!dbSecretHash) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return givenSecretHash === dbSecretHash;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setTotpSecret(secret: string) {
 | 
			
		||||
    if (!secret) {
 | 
			
		||||
        throw new Error("TOTP secret cannot be empty");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const encryptionSalt = randomSecureToken(32);
 | 
			
		||||
    optionService.setOption(TOTP_OPTIONS.SALT, encryptionSalt);
 | 
			
		||||
 | 
			
		||||
    const verificationHash = toBase64(myScryptService.getVerificationHash(secret));
 | 
			
		||||
    optionService.setOption(TOTP_OPTIONS.VERIFICATION_HASH, verificationHash);
 | 
			
		||||
 | 
			
		||||
    const encryptedSecret = dataEncryptionService.encrypt(
 | 
			
		||||
        Buffer.from(encryptionSalt),
 | 
			
		||||
        secret
 | 
			
		||||
    );
 | 
			
		||||
    optionService.setOption(TOTP_OPTIONS.ENCRYPTED_SECRET, encryptedSecret);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTotpSecret(): string | null {
 | 
			
		||||
    const encryptionSalt = optionService.getOptionOrNull(TOTP_OPTIONS.SALT);
 | 
			
		||||
    const encryptedSecret = optionService.getOptionOrNull(TOTP_OPTIONS.ENCRYPTED_SECRET);
 | 
			
		||||
 | 
			
		||||
    if (!encryptionSalt || !encryptedSecret) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const decryptedSecret = dataEncryptionService.decrypt(
 | 
			
		||||
            Buffer.from(encryptionSalt),
 | 
			
		||||
            encryptedSecret
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (!decryptedSecret) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return decryptedSecret.toString();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        console.error("Failed to decrypt TOTP secret:", e);
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resetTotpSecret() {
 | 
			
		||||
    optionService.setOption(TOTP_OPTIONS.SALT, "");
 | 
			
		||||
    optionService.setOption(TOTP_OPTIONS.ENCRYPTED_SECRET, "");
 | 
			
		||||
    optionService.setOption(TOTP_OPTIONS.VERIFICATION_HASH, "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isTotpSecretSet(): boolean {
 | 
			
		||||
    return !!optionService.getOptionOrNull(TOTP_OPTIONS.VERIFICATION_HASH);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    verifyTotpSecret,
 | 
			
		||||
    setTotpSecret,
 | 
			
		||||
    getTotpSecret,
 | 
			
		||||
    resetTotpSecret,
 | 
			
		||||
    isTotpSecretSet
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user