mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	refactoring of password change and preparations for server side encryption
This commit is contained in:
		
							parent
							
								
									433982e7bc
								
							
						
					
					
						commit
						8f1eedfe0d
					
				| @ -131,11 +131,9 @@ const encryption = (function() { | ||||
|     function resetEncryptionSession() { | ||||
|         dataKey = null; | ||||
| 
 | ||||
|         if (noteEditor.getCurrentNote().detail.encryption > 0) { | ||||
|             // most secure solution - guarantees nothing remained in memory
 | ||||
|             // since this expires because user doesn't use the app, it shouldn't be disruptive
 | ||||
|             window.location.reload(true); | ||||
|         } | ||||
|         // most secure solution - guarantees nothing remained in memory
 | ||||
|         // since this expires because user doesn't use the app, it shouldn't be disruptive
 | ||||
|         window.location.reload(true); | ||||
|     } | ||||
| 
 | ||||
|     function isEncryptionAvailable() { | ||||
|  | ||||
| @ -6,8 +6,11 @@ const options = require('../../services/options'); | ||||
| const utils = require('../../services/utils'); | ||||
| const migration = require('../../services/migration'); | ||||
| 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 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; | ||||
| @ -8,8 +8,8 @@ const migration = require('../../services/migration'); | ||||
| 
 | ||||
| router.get('', auth.checkApiAuthWithoutMigration, async (req, res, next) => { | ||||
|     res.send({ | ||||
|         'db_version': parseInt(await options.getOption('db_version')), | ||||
|         'app_db_version': migration.APP_DB_VERSION | ||||
|         db_version: parseInt(await options.getOption('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(); | ||||
| 
 | ||||
|     res.send({ | ||||
|         'migrations': migrations | ||||
|         migrations: migrations | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -5,52 +5,22 @@ const options = require('./options'); | ||||
| const my_scrypt = require('./my_scrypt'); | ||||
| const utils = require('./utils'); | ||||
| const audit_category = require('./audit_category'); | ||||
| const crypto = require('crypto'); | ||||
| const aesjs = require('./aes'); | ||||
| const password_encryption = require('./password_encryption'); | ||||
| 
 | ||||
| async function changePassword(currentPassword, newPassword, req) { | ||||
|     const current_password_hash = utils.toBase64(await my_scrypt.getVerificationHash(currentPassword)); | ||||
| 
 | ||||
|     if (current_password_hash !== await options.getOption('password_verification_hash')) { | ||||
|     if (!await password_encryption.verifyPassword(currentPassword)) { | ||||
|         return { | ||||
|             'success': false, | ||||
|             'message': "Given current password doesn't match hash" | ||||
|             success: false, | ||||
|             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 newPasswordEncryptionKey = await my_scrypt.getPasswordDerivedKey(newPassword); | ||||
|     const newPasswordDerivedKey = await my_scrypt.getPasswordDerivedKey(newPassword); | ||||
| 
 | ||||
|     function decrypt(encryptedBase64) { | ||||
|         const encryptedBytes = utils.fromBase64(encryptedBase64); | ||||
|     const decryptedDataKey = await password_encryption.getDecryptedDataKey(currentPassword); | ||||
| 
 | ||||
|         const aes = getAes(currentPasswordDerivedKey); | ||||
|         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); | ||||
|     const newEncryptedDataKey = password_encryption.encryptDataKey(newPasswordDerivedKey, decryptedDataKey); | ||||
| 
 | ||||
|     await sql.doInTransaction(async () => { | ||||
|         await options.setOption('encrypted_data_key', newEncryptedDataKey); | ||||
| @ -61,8 +31,8 @@ async function changePassword(currentPassword, newPassword, req) { | ||||
|     }); | ||||
| 
 | ||||
|     return { | ||||
|         'success': true, | ||||
|         'new_encrypted_data_key': newEncryptedDataKey | ||||
|         success: true, | ||||
|         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 resp = await syncRequest(syncContext, 'POST', '/api/login', { | ||||
|     const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', { | ||||
|         timestamp: timestamp, | ||||
|         dbVersion: migration.APP_DB_VERSION, | ||||
|         hash: hash | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 azivner
						azivner