2021-08-23 19:47:07 +02:00
import { fileContentEqual , setupDatabaseAndSynchronizer , supportDir , switchClient , objectsEqual , checkThrowAsync , msleep } from '../../testing/test-utils' ;
import Folder from '../../models/Folder' ;
import Note from '../../models/Note' ;
import Setting from '../../models/Setting' ;
import BaseItem from '../../models/BaseItem' ;
import MasterKey from '../../models/MasterKey' ;
2021-10-03 17:00:49 +02:00
import EncryptionService , { EncryptionMethod } from './EncryptionService' ;
2021-08-23 19:47:07 +02:00
import { setEncryptionEnabled } from '../synchronizer/syncInfoUtils' ;
2021-05-21 15:17:21 +02:00
2021-08-07 13:22:37 +02:00
let service : EncryptionService = null ;
2017-12-13 20:57:40 +02:00
2023-02-20 17:02:29 +02:00
describe ( 'services_EncryptionService' , ( ) = > {
2017-12-13 20:57:40 +02:00
2022-11-15 12:23:50 +02:00
beforeEach ( async ( ) = > {
2017-12-13 20:57:40 +02:00
await setupDatabaseAndSynchronizer ( 1 ) ;
2019-05-06 22:35:29 +02:00
await switchClient ( 1 ) ;
2021-01-23 17:51:19 +02:00
service = new EncryptionService ( ) ;
2017-12-13 20:57:40 +02:00
BaseItem . encryptionService_ = service ;
2021-08-12 17:54:10 +02:00
setEncryptionEnabled ( true ) ;
2017-12-13 20:57:40 +02:00
} ) ;
2020-12-01 20:05:24 +02:00
it ( 'should encode and decode header' , ( async ( ) = > {
2017-12-13 20:57:40 +02:00
const header = {
2021-10-03 17:00:49 +02:00
encryptionMethod : EncryptionMethod.SJCL ,
2017-12-13 20:57:40 +02:00
masterKeyId : '01234568abcdefgh01234568abcdefgh' ,
} ;
const encodedHeader = service . encodeHeader_ ( header ) ;
2020-01-23 00:01:58 +02:00
const decodedHeader = service . decodeHeaderBytes_ ( encodedHeader ) ;
2017-12-13 20:57:40 +02:00
delete decodedHeader . length ;
expect ( objectsEqual ( header , decodedHeader ) ) . toBe ( true ) ;
2019-09-24 00:23:10 +02:00
} ) ) ;
2017-12-13 20:57:40 +02:00
2020-12-01 20:05:24 +02:00
it ( 'should generate and decrypt a master key' , ( async ( ) = > {
2017-12-13 20:57:40 +02:00
const masterKey = await service . generateMasterKey ( '123456' ) ;
expect ( ! ! masterKey . content ) . toBe ( true ) ;
let hasThrown = false ;
try {
2021-10-03 17:00:49 +02:00
await service . decryptMasterKeyContent ( masterKey , 'wrongpassword' ) ;
2017-12-13 20:57:40 +02:00
} catch ( error ) {
hasThrown = true ;
}
expect ( hasThrown ) . toBe ( true ) ;
2021-10-03 17:00:49 +02:00
const decryptedMasterKey = await service . decryptMasterKeyContent ( masterKey , '123456' ) ;
2017-12-13 20:57:40 +02:00
expect ( decryptedMasterKey . length ) . toBe ( 512 ) ;
2019-09-24 00:23:10 +02:00
} ) ) ;
2017-12-13 20:57:40 +02:00
2020-12-01 20:05:24 +02:00
it ( 'should upgrade a master key' , ( async ( ) = > {
2020-03-13 19:42:50 +02:00
// Create an old style master key
let masterKey = await service . generateMasterKey ( '123456' , {
2021-10-03 17:00:49 +02:00
encryptionMethod : EncryptionMethod.SJCL2 ,
2020-03-13 19:42:50 +02:00
} ) ;
masterKey = await MasterKey . save ( masterKey ) ;
2021-10-03 17:00:49 +02:00
let upgradedMasterKey = await service . reencryptMasterKey ( masterKey , '123456' , '123456' ) ;
2020-03-13 19:42:50 +02:00
upgradedMasterKey = await MasterKey . save ( upgradedMasterKey ) ;
// Check that master key has been upgraded (different ciphertext)
expect ( masterKey . content ) . not . toBe ( upgradedMasterKey . content ) ;
// Check that master key plain text is still the same
2021-10-03 17:00:49 +02:00
const plainTextOld = await service . decryptMasterKeyContent ( masterKey , '123456' ) ;
const plainTextNew = await service . decryptMasterKeyContent ( upgradedMasterKey , '123456' ) ;
2021-08-12 17:54:10 +02:00
expect ( plainTextOld ) . toBe ( plainTextNew ) ;
2020-03-13 19:42:50 +02:00
// Check that old content can be decrypted with new master key
2021-08-12 17:54:10 +02:00
await service . loadMasterKey ( masterKey , '123456' , true ) ;
2020-03-13 19:42:50 +02:00
const cipherText = await service . encryptString ( 'some secret' ) ;
const plainTextFromOld = await service . decryptString ( cipherText ) ;
2021-08-12 17:54:10 +02:00
await service . loadMasterKey ( upgradedMasterKey , '123456' , true ) ;
2020-03-13 19:42:50 +02:00
const plainTextFromNew = await service . decryptString ( cipherText ) ;
expect ( plainTextFromOld ) . toBe ( plainTextFromNew ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should not upgrade master key if invalid password' , ( async ( ) = > {
2020-03-14 01:46:14 +02:00
const masterKey = await service . generateMasterKey ( '123456' , {
2021-10-03 17:00:49 +02:00
encryptionMethod : EncryptionMethod.SJCL2 ,
2020-03-13 19:42:50 +02:00
} ) ;
2021-10-03 17:00:49 +02:00
await checkThrowAsync ( async ( ) = > await service . reencryptMasterKey ( masterKey , '777' , '777' ) ) ;
2020-03-13 19:42:50 +02:00
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should require a checksum only for old master keys' , ( async ( ) = > {
2020-03-13 19:42:50 +02:00
const masterKey = await service . generateMasterKey ( '123456' , {
2021-10-03 17:00:49 +02:00
encryptionMethod : EncryptionMethod.SJCL2 ,
2020-03-13 19:42:50 +02:00
} ) ;
expect ( ! ! masterKey . checksum ) . toBe ( true ) ;
expect ( ! ! masterKey . content ) . toBe ( true ) ;
} ) ) ;
2024-10-26 22:15:10 +02:00
it . each ( [
EncryptionMethod . SJCL4 ,
EncryptionMethod . KeyV1 ,
] ) ( 'should not require a checksum for new master keys' , ( async ( masterKeyEncryptionMethod ) = > {
2020-03-13 19:42:50 +02:00
const masterKey = await service . generateMasterKey ( '123456' , {
2024-10-26 22:15:10 +02:00
encryptionMethod : masterKeyEncryptionMethod ,
2020-03-13 19:42:50 +02:00
} ) ;
expect ( ! masterKey . checksum ) . toBe ( true ) ;
expect ( ! ! masterKey . content ) . toBe ( true ) ;
2021-10-03 17:00:49 +02:00
const decryptedMasterKey = await service . decryptMasterKeyContent ( masterKey , '123456' ) ;
2020-03-13 19:42:50 +02:00
expect ( decryptedMasterKey . length ) . toBe ( 512 ) ;
} ) ) ;
2024-10-26 22:15:10 +02:00
it . each ( [
EncryptionMethod . SJCL4 ,
EncryptionMethod . KeyV1 ,
] ) ( 'should throw an error if master key decryption fails' , ( async ( masterKeyEncryptionMethod ) = > {
2020-03-13 19:42:50 +02:00
const masterKey = await service . generateMasterKey ( '123456' , {
2024-10-26 22:15:10 +02:00
encryptionMethod : masterKeyEncryptionMethod ,
2020-03-13 19:42:50 +02:00
} ) ;
2021-10-03 17:00:49 +02:00
const hasThrown = await checkThrowAsync ( async ( ) = > await service . decryptMasterKeyContent ( masterKey , 'wrong' ) ) ;
2020-03-13 19:42:50 +02:00
expect ( hasThrown ) . toBe ( true ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should return the master keys that need an upgrade' , ( async ( ) = > {
2020-03-13 19:42:50 +02:00
const masterKey1 = await MasterKey . save ( await service . generateMasterKey ( '123456' , {
2021-10-03 17:00:49 +02:00
encryptionMethod : EncryptionMethod.SJCL2 ,
2020-03-13 19:42:50 +02:00
} ) ) ;
const masterKey2 = await MasterKey . save ( await service . generateMasterKey ( '123456' , {
2021-10-03 17:00:49 +02:00
encryptionMethod : EncryptionMethod.SJCL ,
2020-03-13 19:42:50 +02:00
} ) ) ;
2021-08-07 13:22:37 +02:00
await MasterKey . save ( await service . generateMasterKey ( '123456' ) ) ;
2020-03-13 19:42:50 +02:00
const needUpgrade = service . masterKeysThatNeedUpgrading ( await MasterKey . all ( ) ) ;
expect ( needUpgrade . length ) . toBe ( 2 ) ;
2020-05-21 10:14:33 +02:00
expect ( needUpgrade . map ( k = > k . id ) . sort ( ) ) . toEqual ( [ masterKey1 . id , masterKey2 . id ] . sort ( ) ) ;
2020-03-13 19:42:50 +02:00
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should encrypt and decrypt with a master key' , ( async ( ) = > {
2017-12-13 20:57:40 +02:00
let masterKey = await service . generateMasterKey ( '123456' ) ;
masterKey = await MasterKey . save ( masterKey ) ;
2021-08-12 17:54:10 +02:00
await service . loadMasterKey ( masterKey , '123456' , true ) ;
2017-12-13 20:57:40 +02:00
const cipherText = await service . encryptString ( 'some secret' ) ;
const plainText = await service . decryptString ( cipherText ) ;
expect ( plainText ) . toBe ( 'some secret' ) ;
// Test that a long string, that is going to be split into multiple chunks, encrypt
// and decrypt properly too.
let veryLongSecret = '' ;
2024-10-26 22:15:10 +02:00
for ( let i = 0 ; i < service . chunkSize ( service . defaultEncryptionMethod ( ) ) * 3 ; i ++ ) veryLongSecret += Math . floor ( Math . random ( ) * 9 ) ;
2017-12-13 20:57:40 +02:00
const cipherText2 = await service . encryptString ( veryLongSecret ) ;
const plainText2 = await service . decryptString ( cipherText2 ) ;
expect ( plainText2 === veryLongSecret ) . toBe ( true ) ;
2019-09-24 00:23:10 +02:00
} ) ) ;
2017-12-13 20:57:40 +02:00
2020-12-01 20:05:24 +02:00
it ( 'should decrypt various encryption methods' , ( async ( ) = > {
2020-01-23 00:01:58 +02:00
let masterKey = await service . generateMasterKey ( '123456' ) ;
masterKey = await MasterKey . save ( masterKey ) ;
2021-08-12 17:54:10 +02:00
await service . loadMasterKey ( masterKey , '123456' , true ) ;
2020-01-23 00:01:58 +02:00
{
const cipherText = await service . encryptString ( 'some secret' , {
2021-10-03 17:00:49 +02:00
encryptionMethod : EncryptionMethod.SJCL2 ,
2020-01-23 00:01:58 +02:00
} ) ;
const plainText = await service . decryptString ( cipherText ) ;
expect ( plainText ) . toBe ( 'some secret' ) ;
const header = await service . decodeHeaderString ( cipherText ) ;
2021-10-03 17:00:49 +02:00
expect ( header . encryptionMethod ) . toBe ( EncryptionMethod . SJCL2 ) ;
2020-01-23 00:01:58 +02:00
}
{
const cipherText = await service . encryptString ( 'some secret' , {
2021-10-03 17:00:49 +02:00
encryptionMethod : EncryptionMethod.SJCL3 ,
2020-01-23 00:01:58 +02:00
} ) ;
const plainText = await service . decryptString ( cipherText ) ;
expect ( plainText ) . toBe ( 'some secret' ) ;
const header = await service . decodeHeaderString ( cipherText ) ;
2021-10-03 17:00:49 +02:00
expect ( header . encryptionMethod ) . toBe ( EncryptionMethod . SJCL3 ) ;
2020-01-23 00:01:58 +02:00
}
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should fail to decrypt if master key not present' , ( async ( ) = > {
2017-12-13 20:57:40 +02:00
let masterKey = await service . generateMasterKey ( '123456' ) ;
masterKey = await MasterKey . save ( masterKey ) ;
2021-08-12 17:54:10 +02:00
await service . loadMasterKey ( masterKey , '123456' , true ) ;
2017-12-13 20:57:40 +02:00
const cipherText = await service . encryptString ( 'some secret' ) ;
await service . unloadMasterKey ( masterKey ) ;
2020-03-14 01:46:14 +02:00
const hasThrown = await checkThrowAsync ( async ( ) = > await service . decryptString ( cipherText ) ) ;
2017-12-13 20:57:40 +02:00
expect ( hasThrown ) . toBe ( true ) ;
2019-09-24 00:23:10 +02:00
} ) ) ;
2017-12-13 20:57:40 +02:00
2020-12-01 20:05:24 +02:00
it ( 'should fail to decrypt if data tampered with' , ( async ( ) = > {
2017-12-13 20:57:40 +02:00
let masterKey = await service . generateMasterKey ( '123456' ) ;
masterKey = await MasterKey . save ( masterKey ) ;
2021-08-12 17:54:10 +02:00
await service . loadMasterKey ( masterKey , '123456' , true ) ;
2017-12-13 20:57:40 +02:00
let cipherText = await service . encryptString ( 'some secret' ) ;
2019-07-30 09:35:42 +02:00
cipherText += 'ABCDEFGHIJ' ;
2017-12-13 20:57:40 +02:00
2020-03-14 01:46:14 +02:00
const hasThrown = await checkThrowAsync ( async ( ) = > await service . decryptString ( cipherText ) ) ;
2017-12-13 20:57:40 +02:00
expect ( hasThrown ) . toBe ( true ) ;
2019-09-24 00:23:10 +02:00
} ) ) ;
2017-12-13 20:57:40 +02:00
2024-10-26 22:15:10 +02:00
it . each ( [
EncryptionMethod . SJCL1a ,
EncryptionMethod . SJCL1b ,
EncryptionMethod . SJCL4 ,
EncryptionMethod . KeyV1 ,
EncryptionMethod . FileV1 ,
EncryptionMethod . StringV1 ,
] ) ( 'should fail to decrypt if ciphertext is not a valid JSON string' , ( async ( jsonCipherTextMethod ) = > {
const masterKey = await service . generateMasterKey ( '123456' ) ;
const masterKeyContent = await service . decryptMasterKeyContent ( masterKey , '123456' ) ;
const cipherTextString = await service . encrypt ( jsonCipherTextMethod , masterKeyContent , 'e21de21d' ) ; // 'e21de21d' is a valid base64/hex string
// Check if decryption is working
const plainText = await service . decrypt ( jsonCipherTextMethod , masterKeyContent , cipherTextString ) ;
expect ( plainText ) . toBe ( 'e21de21d' ) ;
// Make invalid JSON
const invalidCipherText = cipherTextString . replace ( '{' , '{,' ) ;
const hasThrown = await checkThrowAsync ( async ( ) = > await service . decrypt ( jsonCipherTextMethod , masterKeyContent , invalidCipherText ) ) ;
expect ( hasThrown ) . toBe ( true ) ;
} ) ) ;
it . each ( [
EncryptionMethod . SJCL1a ,
EncryptionMethod . SJCL1b ,
EncryptionMethod . SJCL4 ,
EncryptionMethod . KeyV1 ,
EncryptionMethod . FileV1 ,
EncryptionMethod . StringV1 ,
] ) ( 'should fail to decrypt if ciphertext authentication failed' , ( async ( authenticatedEncryptionMethod ) = > {
const masterKey = await service . generateMasterKey ( '123456' ) ;
const masterKeyContent = await service . decryptMasterKeyContent ( masterKey , '123456' ) ;
const cipherTextObject = JSON . parse ( await service . encrypt ( authenticatedEncryptionMethod , masterKeyContent , 'e21de21d' ) ) ; // 'e21de21d' is a valid base64/hex string
expect ( cipherTextObject ) . toHaveProperty ( 'ct' ) ;
const ct = Buffer . from ( cipherTextObject [ 'ct' ] , 'base64' ) ;
// Should not fail if the binary data of ct is not modified
const oldCipherTextObject = { . . . cipherTextObject , ct : ct.toString ( 'base64' ) } ;
const plainText = await service . decrypt ( authenticatedEncryptionMethod , masterKeyContent , JSON . stringify ( oldCipherTextObject ) ) ;
expect ( plainText ) . toBe ( 'e21de21d' ) ;
// The encrypted data part is changed so it doesn't match the authentication tag. Decryption should fail.
ct [ 0 ] ^= 0x55 ;
const newCipherTextObject = { . . . cipherTextObject , ct : ct.toString ( 'base64' ) } ;
const hasThrown = await checkThrowAsync ( async ( ) = > service . decrypt ( authenticatedEncryptionMethod , masterKeyContent , JSON . stringify ( newCipherTextObject ) ) ) ;
expect ( hasThrown ) . toBe ( true ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should encrypt and decrypt notes and folders' , ( async ( ) = > {
2017-12-13 20:57:40 +02:00
let masterKey = await service . generateMasterKey ( '123456' ) ;
masterKey = await MasterKey . save ( masterKey ) ;
2021-08-12 17:54:10 +02:00
await service . loadMasterKey ( masterKey , '123456' , true ) ;
2017-12-13 20:57:40 +02:00
2020-03-14 01:46:14 +02:00
const folder = await Folder . save ( { title : 'folder' } ) ;
const note = await Note . save ( { title : 'encrypted note' , body : 'something' , parent_id : folder.id } ) ;
const serialized = await Note . serializeForSync ( note ) ;
const deserialized = Note . filter ( await Note . unserialize ( serialized ) ) ;
2017-12-13 20:57:40 +02:00
// Check that required properties are not encrypted
expect ( deserialized . id ) . toBe ( note . id ) ;
expect ( deserialized . parent_id ) . toBe ( note . parent_id ) ;
expect ( deserialized . updated_time ) . toBe ( note . updated_time ) ;
// Check that at least title and body are encrypted
expect ( ! deserialized . title ) . toBe ( true ) ;
expect ( ! deserialized . body ) . toBe ( true ) ;
// Check that encrypted data is there
expect ( ! ! deserialized . encryption_cipher_text ) . toBe ( true ) ;
2019-07-30 09:35:42 +02:00
const encryptedNote = await Note . save ( deserialized ) ;
const decryptedNote = await Note . decrypt ( encryptedNote ) ;
2017-12-13 20:57:40 +02:00
expect ( decryptedNote . title ) . toBe ( note . title ) ;
expect ( decryptedNote . body ) . toBe ( note . body ) ;
expect ( decryptedNote . id ) . toBe ( note . id ) ;
expect ( decryptedNote . parent_id ) . toBe ( note . parent_id ) ;
2019-09-24 00:23:10 +02:00
} ) ) ;
2017-12-13 20:57:40 +02:00
2024-10-26 22:15:10 +02:00
it . each ( [
EncryptionMethod . SJCL1a ,
EncryptionMethod . SJCL1b ,
EncryptionMethod . FileV1 ,
EncryptionMethod . StringV1 ,
] ) ( 'should encrypt and decrypt files' , ( async ( fileEncryptionMethod ) = > {
2017-12-18 21:54:03 +02:00
let masterKey = await service . generateMasterKey ( '123456' ) ;
masterKey = await MasterKey . save ( masterKey ) ;
2021-08-12 17:54:10 +02:00
await service . loadMasterKey ( masterKey , '123456' , true ) ;
2017-12-18 21:54:03 +02:00
2021-05-21 15:17:21 +02:00
const sourcePath = ` ${ supportDir } /photo.jpg ` ;
2020-12-09 17:06:14 +02:00
const encryptedPath = ` ${ Setting . value ( 'tempDir' ) } /photo.crypted ` ;
const decryptedPath = ` ${ Setting . value ( 'tempDir' ) } /photo.jpg ` ;
2017-12-18 21:54:03 +02:00
2024-10-26 22:15:10 +02:00
service . defaultFileEncryptionMethod_ = fileEncryptionMethod ;
2017-12-18 21:54:03 +02:00
await service . encryptFile ( sourcePath , encryptedPath ) ;
await service . decryptFile ( encryptedPath , decryptedPath ) ;
expect ( fileContentEqual ( sourcePath , encryptedPath ) ) . toBe ( false ) ;
expect ( fileContentEqual ( sourcePath , decryptedPath ) ) . toBe ( true ) ;
2019-09-24 00:23:10 +02:00
} ) ) ;
2017-12-18 21:54:03 +02:00
2024-10-26 22:15:10 +02:00
it . each ( [
EncryptionMethod . SJCL1a ,
EncryptionMethod . SJCL1b ,
EncryptionMethod . StringV1 ,
] ) ( 'should encrypt invalid UTF-8 data' , ( async ( stringEncryptionMethod ) = > {
2020-03-06 21:11:51 +02:00
let masterKey = await service . generateMasterKey ( '123456' ) ;
masterKey = await MasterKey . save ( masterKey ) ;
2021-08-12 17:54:10 +02:00
await service . loadMasterKey ( masterKey , '123456' , true ) ;
2020-03-06 21:11:51 +02:00
// First check that we can replicate the error with the old encryption method
2021-10-03 17:00:49 +02:00
service . defaultEncryptionMethod_ = EncryptionMethod . SJCL ;
2023-01-11 20:37:22 +02:00
const hasThrown = await checkThrowAsync ( async ( ) = > await service . encryptString ( '🐶🐶🐶' . substr ( 0 , 5 ) ) ) ;
2020-03-06 21:11:51 +02:00
expect ( hasThrown ) . toBe ( true ) ;
// Now check that the new one fixes the problem
2024-10-26 22:15:10 +02:00
service . defaultEncryptionMethod_ = stringEncryptionMethod ;
2023-01-11 20:37:22 +02:00
const cipherText = await service . encryptString ( '🐶🐶🐶' . substr ( 0 , 5 ) ) ;
2020-03-06 21:11:51 +02:00
const plainText = await service . decryptString ( cipherText ) ;
2023-01-11 20:37:22 +02:00
expect ( plainText ) . toBe ( '🐶🐶🐶' . substr ( 0 , 5 ) ) ;
2020-03-06 21:11:51 +02:00
} ) ) ;
2021-08-12 17:54:10 +02:00
it ( 'should check if a master key is loaded' , ( async ( ) = > {
let masterKey = await service . generateMasterKey ( '123456' ) ;
masterKey = await MasterKey . save ( masterKey ) ;
await service . loadMasterKey ( masterKey , '123456' , true ) ;
expect ( service . isMasterKeyLoaded ( masterKey ) ) . toBe ( true ) ;
await msleep ( 1 ) ;
// If the master key is modified afterwards it should report that it is
// *not* loaded since it doesn't have this new version.
masterKey = await MasterKey . save ( masterKey ) ;
expect ( service . isMasterKeyLoaded ( masterKey ) ) . toBe ( false ) ;
} ) ) ;
2021-10-03 17:00:49 +02:00
2019-07-30 09:35:42 +02:00
} ) ;