Current Implementation
According to the technical specifications and the code, When encryption is enabled, the following steps are performed:
- User inputs password which is stored in the local SQLite database.
- A random 256-byte master key (or shorter) is generated.
- The master key is encrypted using the
AES-256-CCMencryption method. The encryption key for the master key is derived from the password using thePBKDF2function (source1, source2). You can find the encrypted master key in theinfo.jsonin your sync target. - Notes and resources are encrypted using the
AES-128-CCMencryption method. The data is not directly encrypted by the master key. We use the master key and a randomsaltto generate the actual encryption key withPBKDF2function, just like how we generate the encryption key for encrypting master key.
Notes:
- The
PBKDF2implementation insjclis slow. However,sjclcached the key derivation output with the same parameters (password, salt, iteration count), so reusing the same derivation parameters for multiple times is faster than expected. - The
saltof the data encryption key changes when Joplin restarts. This brings some resistance of reused nonce vulnerability because the generated data encryption key changes. However, we can set a maximum size threshold to reset thesaltforcefully in order to increase the security. - The chunk size is 5k bytes due to the slowness of
sjcl. Considering we could get a much higher encryption speed (base64 encoding/decoding is not included) by switching to native encryption library, we can set the chunk size to a higher value like 16k or 64k - The size overhead should be around 33.3% due to the base64 encoding. However, when checking the ciphertext on the sync target, I found the size overhead is more than 77.7%. It looks like the base64 encoding is performed twice. I'm now finding the reason of it.
Future Considerations
The key derivation function: PBKDF2
PBKDF2is widely supported so I choose it. This is also whatsjcluses so the security of this part won't decrease. There are a few better key derivation functions likescryptandArgon2but they are not available innode:crypto/react-native-quick-crypto- Increase the key iteration count of encrypting master key from 10000 to 210000 (suggested by OWASP). The native implementation of PBKDF2 is around 200 times faster than
sjcl, with this key iteration count we can get higher security and faster speed. - Change the salt length from 64 bit to 128 bit (or higher). 64 bit is the minimum requirement of
PBKDF2and we'd like to be higher than the boundary. Also the 128 bit salt length is widely used now. - Change the digest algorithm from
SHA-256toSHA-512for longer key length. The extra part could be used in the future. scryptmight be a better choice once it's available inreact-native-quick-crypto.
The cipher and the mode: AES-GCM
AES-GCMis widely supported so I choose it. There are a few better ciphers/modes but they are not available innode:crypto/react-native-quick-crypto.- Change the key size from 128 bits to 256 bits. We've tried it before but reverted it due to the performance issues. Now with the powerful native implementation we are good to use it.
- Change the IV size to 12 bytes (96 bits). This is recommended for the
AES-GCMmode. We could use a longer IV but forAES-GCMit will be hashed to 96 bits finally. As for the potential nonce(IV) collision, we use the derived key from master key with a random salt to mitigate it.- The default IV size in
sjclis 16 bytes, but for the oldCCMmode only the first 13 bytes are used. We use 12 bytes forGCMmode so it's easier to get collision (though in a small possibility) but as I noted before we will use a longer and frequently refreshedsaltto mitigate this.
- The default IV size in
- Change the authentication tag size from 64 bits to 128 bits (suggested in this paper).
- As far as I know the known defects about
AES-GCMare only about leaking the authentication key, which doesn't matter for Joplin.
Please feel free to discuss the old/new encryption parameters.