mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-11-04 15:11:31 +08:00 
			
		
		
		
	refactoring of password change and preparations for server side encryption
This commit is contained in:
		
							parent
							
								
									433982e7bc
								
							
						
					
					
						commit
						8f1eedfe0d
					
				@ -131,12 +131,10 @@ const encryption = (function() {
 | 
				
			|||||||
    function resetEncryptionSession() {
 | 
					    function resetEncryptionSession() {
 | 
				
			||||||
        dataKey = null;
 | 
					        dataKey = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (noteEditor.getCurrentNote().detail.encryption > 0) {
 | 
					 | 
				
			||||||
        // most secure solution - guarantees nothing remained in memory
 | 
					        // most secure solution - guarantees nothing remained in memory
 | 
				
			||||||
        // since this expires because user doesn't use the app, it shouldn't be disruptive
 | 
					        // since this expires because user doesn't use the app, it shouldn't be disruptive
 | 
				
			||||||
        window.location.reload(true);
 | 
					        window.location.reload(true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function isEncryptionAvailable() {
 | 
					    function isEncryptionAvailable() {
 | 
				
			||||||
        return dataKey !== null;
 | 
					        return dataKey !== null;
 | 
				
			||||||
 | 
				
			|||||||
@ -6,8 +6,11 @@ const options = require('../../services/options');
 | 
				
			|||||||
const utils = require('../../services/utils');
 | 
					const utils = require('../../services/utils');
 | 
				
			||||||
const migration = require('../../services/migration');
 | 
					const migration = require('../../services/migration');
 | 
				
			||||||
const SOURCE_ID = require('../../services/source_id');
 | 
					const SOURCE_ID = require('../../services/source_id');
 | 
				
			||||||
 | 
					const auth = require('../../services/auth');
 | 
				
			||||||
 | 
					const password_encryption = require('../../services/password_encryption');
 | 
				
			||||||
 | 
					const protected_session = require('../../services/protected_session');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.post('', async (req, res, next) => {
 | 
					router.post('/sync', async (req, res, next) => {
 | 
				
			||||||
    const timestamp = req.body.timestamp;
 | 
					    const timestamp = req.body.timestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const now = utils.nowTimestamp();
 | 
					    const now = utils.nowTimestamp();
 | 
				
			||||||
@ -41,4 +44,25 @@ router.post('', async (req, res, next) => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
 | 
				
			||||||
 | 
					router.post('protected', auth.checkApiAuth, async (req, res, next) => {
 | 
				
			||||||
 | 
					    const password = req.body.password;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!await password_encryption.verifyPassword(password)) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            success: false,
 | 
				
			||||||
 | 
					            message: "Given current password doesn't match hash"
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const decryptedDataKey = password_encryption.getDecryptedDataKey(password);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const protectedSessionId = protected_session.setDataKey(req, decryptedDataKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.send({
 | 
				
			||||||
 | 
					        success: true,
 | 
				
			||||||
 | 
					        protectedSessionId: protectedSessionId
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
@ -8,8 +8,8 @@ const migration = require('../../services/migration');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
router.get('', auth.checkApiAuthWithoutMigration, async (req, res, next) => {
 | 
					router.get('', auth.checkApiAuthWithoutMigration, async (req, res, next) => {
 | 
				
			||||||
    res.send({
 | 
					    res.send({
 | 
				
			||||||
        'db_version': parseInt(await options.getOption('db_version')),
 | 
					        db_version: parseInt(await options.getOption('db_version')),
 | 
				
			||||||
        'app_db_version': migration.APP_DB_VERSION
 | 
					        app_db_version: migration.APP_DB_VERSION
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,7 +17,7 @@ router.post('', auth.checkApiAuthWithoutMigration, async (req, res, next) => {
 | 
				
			|||||||
    const migrations = await migration.migrate();
 | 
					    const migrations = await migration.migrate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send({
 | 
					    res.send({
 | 
				
			||||||
        'migrations': migrations
 | 
					        migrations: migrations
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,52 +5,22 @@ const options = require('./options');
 | 
				
			|||||||
const my_scrypt = require('./my_scrypt');
 | 
					const my_scrypt = require('./my_scrypt');
 | 
				
			||||||
const utils = require('./utils');
 | 
					const utils = require('./utils');
 | 
				
			||||||
const audit_category = require('./audit_category');
 | 
					const audit_category = require('./audit_category');
 | 
				
			||||||
const crypto = require('crypto');
 | 
					const password_encryption = require('./password_encryption');
 | 
				
			||||||
const aesjs = require('./aes');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function changePassword(currentPassword, newPassword, req) {
 | 
					async function changePassword(currentPassword, newPassword, req) {
 | 
				
			||||||
    const current_password_hash = utils.toBase64(await my_scrypt.getVerificationHash(currentPassword));
 | 
					    if (!await password_encryption.verifyPassword(currentPassword)) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (current_password_hash !== await options.getOption('password_verification_hash')) {
 | 
					 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            'success': false,
 | 
					            success: false,
 | 
				
			||||||
            'message': "Given current password doesn't match hash"
 | 
					            message: "Given current password doesn't match hash"
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const currentPasswordDerivedKey = await my_scrypt.getPasswordDerivedKey(currentPassword);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const newPasswordVerificationKey = utils.toBase64(await my_scrypt.getVerificationHash(newPassword));
 | 
					    const newPasswordVerificationKey = utils.toBase64(await my_scrypt.getVerificationHash(newPassword));
 | 
				
			||||||
    const newPasswordEncryptionKey = await my_scrypt.getPasswordDerivedKey(newPassword);
 | 
					    const newPasswordDerivedKey = await my_scrypt.getPasswordDerivedKey(newPassword);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function decrypt(encryptedBase64) {
 | 
					    const decryptedDataKey = await password_encryption.getDecryptedDataKey(currentPassword);
 | 
				
			||||||
        const encryptedBytes = utils.fromBase64(encryptedBase64);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const aes = getAes(currentPasswordDerivedKey);
 | 
					    const newEncryptedDataKey = password_encryption.encryptDataKey(newPasswordDerivedKey, decryptedDataKey);
 | 
				
			||||||
        return aes.decrypt(encryptedBytes).slice(4);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function encrypt(plainText) {
 | 
					 | 
				
			||||||
        const aes = getAes(newPasswordEncryptionKey);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const plainTextBuffer = Buffer.from(plainText, 'latin1');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const digest = crypto.createHash('sha256').update(plainTextBuffer).digest().slice(0, 4);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const encryptedBytes = aes.encrypt(Buffer.concat([digest, plainTextBuffer]));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return utils.toBase64(encryptedBytes);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function getAes(key) {
 | 
					 | 
				
			||||||
        return new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const encryptedDataKey = await options.getOption('encrypted_data_key');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const decryptedDataKey = decrypt(encryptedDataKey);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const newEncryptedDataKey = encrypt(decryptedDataKey);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sql.doInTransaction(async () => {
 | 
					    await sql.doInTransaction(async () => {
 | 
				
			||||||
        await options.setOption('encrypted_data_key', newEncryptedDataKey);
 | 
					        await options.setOption('encrypted_data_key', newEncryptedDataKey);
 | 
				
			||||||
@ -61,8 +31,8 @@ async function changePassword(currentPassword, newPassword, req) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        'success': true,
 | 
					        success: true,
 | 
				
			||||||
        'new_encrypted_data_key': newEncryptedDataKey
 | 
					        new_encrypted_data_key: newEncryptedDataKey
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										53
									
								
								services/password_encryption.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								services/password_encryption.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					const options = require('./options');
 | 
				
			||||||
 | 
					const my_scrypt = require('./my_scrypt');
 | 
				
			||||||
 | 
					const utils = require('./utils');
 | 
				
			||||||
 | 
					const crypto = require('crypto');
 | 
				
			||||||
 | 
					const aesjs = require('./aes');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function verifyPassword(password) {
 | 
				
			||||||
 | 
					    const givenPasswordHash = utils.toBase64(await my_scrypt.getVerificationHash(password));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const dbPasswordHash = await options.getOption('password_verification_hash');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return givenPasswordHash === dbPasswordHash;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function decryptDataKey(passwordDerivedKey, encryptedBase64) {
 | 
				
			||||||
 | 
					    const encryptedBytes = utils.fromBase64(encryptedBase64);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const aes = getAes(passwordDerivedKey);
 | 
				
			||||||
 | 
					    return aes.decrypt(encryptedBytes).slice(4);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function encryptDataKey(passwordDerivedKey, plainText) {
 | 
				
			||||||
 | 
					    const aes = getAes(passwordDerivedKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const plainTextBuffer = Buffer.from(plainText, 'latin1');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const digest = crypto.createHash('sha256').update(plainTextBuffer).digest().slice(0, 4);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const encryptedBytes = aes.encrypt(Buffer.concat([digest, plainTextBuffer]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return utils.toBase64(encryptedBytes);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getDecryptedDataKey(password) {
 | 
				
			||||||
 | 
					    const passwordDerivedKey = await my_scrypt.getPasswordDerivedKey(password);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const encryptedDataKey = await options.getOption('encrypted_data_key');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const decryptedDataKey = decryptDataKey(passwordDerivedKey, encryptedDataKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return decryptedDataKey;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getAes(key) {
 | 
				
			||||||
 | 
					    return new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					    verifyPassword,
 | 
				
			||||||
 | 
					    decryptDataKey,
 | 
				
			||||||
 | 
					    encryptDataKey,
 | 
				
			||||||
 | 
					    getDecryptedDataKey
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										22
									
								
								services/protected_session.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								services/protected_session.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					const utils = require('./utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setDataKey(req, decryptedDataKey) {
 | 
				
			||||||
 | 
					    req.session.decryptedDataKey = decryptedDataKey;
 | 
				
			||||||
 | 
					    req.session.protectedSessionId = utils.randomSecureToken(32);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return req.session.protectedSessionId;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getDataKey(req, protectedSessionId) {
 | 
				
			||||||
 | 
					    if (protectedSessionId && req.session.protectedSessionId === protectedSessionId) {
 | 
				
			||||||
 | 
					        return req.session.decryptedDataKey;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					    setDataKey,
 | 
				
			||||||
 | 
					    getDataKey
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -82,7 +82,7 @@ async function login() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const syncContext = { cookieJar: rp.jar() };
 | 
					    const syncContext = { cookieJar: rp.jar() };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const resp = await syncRequest(syncContext, 'POST', '/api/login', {
 | 
					    const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
 | 
				
			||||||
        timestamp: timestamp,
 | 
					        timestamp: timestamp,
 | 
				
			||||||
        dbVersion: migration.APP_DB_VERSION,
 | 
					        dbVersion: migration.APP_DB_VERSION,
 | 
				
			||||||
        hash: hash
 | 
					        hash: hash
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user