It is important that there exist a separation of concerns between the server and the client. That is, the client should not trust the server, and vice versa.
Encryption keys are generated by stretching the user's input password using a key derivation function.
The resulting key is split in three — the first third is sent to the server as the user's password, the second third is saved locally as the user's master encryption key, and the last third is used as an authentication key. In this setup, the server is never able to compute the encryption key or the user's original password given just a fraction of the resulting key.
Note: client-server connections must be made securely through SSL/TLS.
|pw_cost||The number of iterations to be used by the KDF. The minimum for version 003 is 100,000. However note that non-native clients (web clients not using WebCrypto) will not be able to handle any more than 3,000 iterations.|
|pw_nonce||A nonce for password derivation. This value is initially created by the client during registration.|
Given a user inputted password
uip, the client's job is to generate a password
pw to send to the server, a master key
mk that the user stores locally to encrypt/decrypt data, and an auth key
ak for authenticating encrypted data.
auth/paramsto retrieve password nonce, cost, and version.
ak using PBKDF2 with SHA512 as the hashing function and output length of 768 bits:
salt = SHA256:Hexdigest([email, "SF", version, pw_cost, pw_nonce].join(":")) key = pbkdf2(uip, salt, sha512, 768, pw_cost) // hex encoded pw = key.substring(0, key.length/3) mk = key.substring(key.length/3, key.length/3) ak = key.substring(key.length/3 * 2, key.length/3)
pw to the server as the user's "regular" password and stores
ak locally. (
ak are never sent to the server).
pw_nonce = random_string(256)
ak using step (3) from Login Steps.
Client registers with
In general, when encrypting a string, one should use an IV so that two subsequent encryptions of the same content yield different results, and one should authenticate the data as to ascertain its authenticity and lack of tampering.
In Standard Notes, two strings are encrypted for every item:
An item is encrypted using a random key generated for each item.
Note that when encrypting/decrypting data, keys should be converted to the proper format your platform function supports. It's best to convert keys to binary data before running through any encryption/hashing algorithm.
For every item:
item_key(in hex format).
item_keyin half; set item encryption key
item_ek = first_halfand item authentication key
item_ak = second_half.
item_akfollowing the instructions "Encrypting a string using the 003 scheme" below and send to server as
item_keyusing the global
akfollowing the instructions "Encrypting a string using the 003 scheme" below and send to server as
Check the first 3 characters of the
content string. This will be the encryption version.
If it is equal to "001", which is a legacy scheme, decrypt according to the 001 instructions found here.
If it is equal to "002" or "003", decrypt as follows:
enc_item_keyusing the global
akaccording to the "Decrypting a string using the 003 scheme" instructions below to get
item_keyin half; set encryption key
item_ek = first_halfand authentication key
item_ak = second_half.
item_akaccording to the "Decrypting a string using the 003 scheme" instructions below.
encryption_key, and an
ciphertext = AES-Encrypt(string_to_encrypt, encryption_key, IV)
string_to_authby combining the encryption version (003), the item's UUID, the IV, and the ciphertext using the colon ":" character:
string_to_auth = ["003", uuid, IV, ciphertext].join(":")
auth_hash = HMAC-SHA256:Hex(string_to_auth, auth_key).
result = ["003", auth_hash, uuid, IV, ciphertext].join(":")
encryption_key, and an
components = string_to_decrypt.split(":").
version = components auth_hash = components uuid = components IV = components ciphertext = components
uuid == item.uuid. If not, abort decryption.
string_to_auth = [version, uuid, IV, ciphertext].join(":").
local_auth_hash = HMAC-SHA256(string_to_auth, auth_key). Compare
auth_hash. If they are not the same, skip decrypting this item, as this indicates that the string has been tampered with.
ciphertextto get final result:
result = AES-Decrypt(ciphertext, encryption_key, IV).
For every received item:
contentusing server known key and store. Decrypt before sending back to client.
Join the Slack group to discuss implementation details and ask any questions you may have.
You can also email email@example.com.
Follow @standardnotes for updates and announcements.