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-CCM
encryption method. The encryption key for the master key is derived from the password using thePBKDF2
function (source1, source2). You can find the encrypted master key in theinfo.json
in your sync target. - Notes and resources are encrypted using the
AES-128-CCM
encryption method. The data is not directly encrypted by the master key. We use the master key and a randomsalt
to generate the actual encryption key withPBKDF2
function, just like how we generate the encryption key for encrypting master key.
Notes:
- The
PBKDF2
implementation insjcl
is slow. However,sjcl
cached 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
salt
of 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 thesalt
forcefully 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
PBKDF2
is widely supported so I choose it. This is also whatsjcl
uses so the security of this part won't decrease. There are a few better key derivation functions likescrypt
andArgon2
but 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
PBKDF2
and 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-256
toSHA-512
for longer key length. The extra part could be used in the future. scrypt
might be a better choice once it's available inreact-native-quick-crypto
.
The cipher and the mode: AES-GCM
AES-GCM
is 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-GCM
mode. We could use a longer IV but forAES-GCM
it 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
sjcl
is 16 bytes, but for the oldCCM
mode only the first 13 bytes are used. We use 12 bytes forGCM
mode so it's easier to get collision (though in a small possibility) but as I noted before we will use a longer and frequently refreshedsalt
to 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-GCM
are only about leaking the authentication key, which doesn't matter for Joplin.
Please feel free to discuss the old/new encryption parameters.