mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-11-01 13:31:32 +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() { |     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() { | ||||||
|  | |||||||
| @ -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
	 azivner
						azivner