-
Notifications
You must be signed in to change notification settings - Fork 5
Encryption details
- When you protect shared dials with a password, all dials data will be encrypted on your device before they are uploaded to the server.
- Your password is never stored anywhere and cannot be recovered.
- Anyone who tries to access your shared dials (with your shared link) will not be able to use offline brute-force attack to crack your password (encrypted data are not accessible without "proof of password").
The rest of this article will explain in detail how is the encryption implemented.
-
When user enters his password to protect the data, the password is first hashed using PBKDF2 with SHA-256 with 5 million iterations and random 128 bit "SALT".
The result of this operation is "HASH", which can be locally stored in the memory or in the browser "sessionStorage" (which is cleared when the tab is discarded). -
From the result "HASH" we compute "ACCESS_SECRET" (like a CRC) by hashing it with SHA-256.
This "ACCESS_SECRET" will be uploaded to the server with encrypted data and server will return encrypted data ONLY if you provide correct "ACCESS_SECRET". -
Before we encrypt the data, we will generate strong random master password.
We are usingcrypto.subtle.generateKeywith "AES-GCM" to generate 256 bit long key.
This key is wrapped with the wrapping key which we get by usingcrypto.subtle.deriveKeyon the user password "HASH" (see step 1).
Wrapped master password will be stored with encrypted data on the server as "SECRET". Note that you still need user password "HASH" to unwrap the actual master password! -
Data are encrypted using the generated master password.
We are usingcrypto.subtle.encryptwith "AES-GCM" with random "IV" (96 bit) and the unwrapped master password. Ecnrypted data will be uploaded to the server as "CIPHERTEXT". -
Data are uploaded to the server, specifically:
- name of the hashing function (SHA-256), algorithm (PBKDF2) and number of iterations (5,000,000)
- "ACCESS_SECRET" - to verify that the user that requests the encrypted data can compute correct 256 bit long hash of the hashed password
- "CIPHERTEXT" - the actual encrypted data
- "SECRET" - wrapped master password (needs to be unwrapped with user password "HASH" before it can be used to decrypt the data)
- "SALT" - random password salt
- "IV" - random initialization vector
-
Data are stored in the database and UUID (converted to base64) is returned. This ID is then used to create sharing link, for example: "/cloud/?share=E6oL0frVQ4qoN51im-wrNQ"
-
When user opens shared link to the encrypted dials data, he will receive following data from the server:
- name of the hashing function (SHA-256), algorithm (PBKDF2) and number of iterations (5e6)
- "SALT"- that needs to be added to the user password
-
User is now presented with a password input.
-
Entered password is merged with "SALT" and hashed using PBKDF2 with SHA-256 using 5e6 iterations (this creates "HASH" of the password).
The result "HASH" is hashed using SHA-256 to create "ACCESS_SECRET". -
Browser will send new request to the server, this time with "ACCESS_SECRET" (in a header).
-
Server will compare "ACCESS_SECRET" with the "ACCESS_SECRET" in the database using "timingSafeEqual" operation.
If the secret matches, server will reply with the missing data - CIPHERTEXT, SECRET and IV. -
Browser will unwrap "SECRET" using the "HASH" - which will give us master password. The "CIPHERTEXT" is then decrypted using this master password and "IV".
Since attacker that tries to access the encrypted data doesn't receive actual data nor wrapped master password, there is no possibility for offline attack.
Attacker would have to compute complex hash and submit it to the server to test it.
Server API is protected with "leaky bucket" so attacker would be limited to test ~1 password per second (per IP).
Additionally, server is running with 1 CPU (1 thread) so testing huge amount of passwords is simply not computationally possible.
End-to-End encryption in the dials synchronizing is very similar to the shared dials encryption described above (it uses the same encryption library).
Let's look at the real world example. This is a request payload sent by the browser to the server:
And here is the explanation of individual fields in the payload:
{
// Your groups data have been replaced with these two dummy dials that explains E2EE situation for the old unsupported old versions
"groups": [[
0, "E2E", 1, 2, [
["E2E encryption enabled ⚠️", 3, 0, 0, 0, 0, 0, 0, 1, 0, ["Data are encrypted.\nIf you don't see a password prompt, update your Group Speed Dial extension.", 0]],
["More info 🔑", 0, "https://github.com/fastaddons/GroupSpeedDial/wiki/Data-synchronization-with-End%E2%80%90to%E2%80%90End-encryption", 0, [2, "./svg/question.svg", 0, 0, 0, null], 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, [0, 16, 1]],
],
[16, 12, false],
]],
"metadata": {
// This indicates that E2E encryption is enabled
"e2e": true,
// Encryption information about the main "CryptoKey":
"masterPassword": {
"id": 1768809902450,
"name": "master password",
"options": {
// Name of the hashing algorithm used:
"hash": "SHA-256",
// Hint you provided when setting the master password:
"hint": "aaa",
// Number of iterations used to derive the CryptoKey from your password:
"iterations": 1000000,
// Name of the key derivation function used:
"name": "PBKDF2",
// Recovery salt:
"recoverySalt": [125, 113, 82, 162, 175, 188, 180, 208, 153, 225, 252, 42, 103, 81, 88, 210],
// Recovery secret - this the main `CryptoKey` encrypted (wrapped with `crypto.subtle.wrapKey`) with your recovery key:
"recoverySecret": "W£çb�ŽÔ�¦ÓÞC3ÀÁ\u0003¢\u000eÚdÀç Ñ\u0018\u0004ÜNP\u0015£Ê~Tò�\nïô",
// Salt used to derive the encryption key from your password:
"salt": [23, 164, 174, 248, 130, 234, 149, 133, 209, 154, 19, 175, 76, 75, 57, 35],
// This is the main `CryptoKey` encrypted with your password:
"secret": "Ä3B\n×!�Z`Ê�ÜSi®`��7«âH³Ù±\u0001ÎÊØýÚ��1�L�¶O\r",
},
},
"encryptedData": {
// These are your actual encrypted dials data. They are first compressed with DEFLATE, then encrypted using randomly generated `CryptoKey`, then converted to Base64 string:
"ciphertext": "/6bdHiLB2mO0ehAwx5Q4VDZzcz7KHekwZAsp8c8Ir78E8GO9ok59F5piuTtyYptvT7d5TeXIZvRP4wEucOo4CjLmdAYoa9u+9wr2jso...<truncated a very long line>",
// Initialization vector used for the encryption:
"iv": [120, 82, 85, 83, 41, 154, 98, 44, 85, 163, 117, 52],
// Encoding type - it will be always "1" which means Base64 encoding:
"en": 1,
// Compression type - it will be always "2" which means DEFLATE compression:
"cp": 2,
},
},
"sync": {},
}
- your data are encrypted using randomly generated
CryptoKey(a 256 bits long AES-GCM key) - this key is stored locally on the disk - in the
IndexedDB, this way you don't have to re-enter your password every time your data synchronizes - your password is never stored anywhere - it's used only to (un)wrap the main
CryptoKey(if it's missing) from the "secret" field - the main
CryptoKeyis encrypted and stored twice - once with your password (stored in "secret" field) and once with the random 56 digits long "Recovery key" (stored in "recoverySecret" field) - if you change your password, new
CryptoKeyis created and your data are re-encrypted using this new key