Encryption

Version 0.0.3

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.

Elaboration on User model encryption related fields

name details
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.

Key Generation

Client Instructions

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.

Login Steps

  1. Client makes GET request with user's email to auth/params to retrieve password nonce, cost, and version.
  2. Client verifies cost >= minimum cost (100,000 for 003.)
  3. Client computes pw, mk, and 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)
    
  4. Client sends pw to the server as the user's "regular" password and stores mk and ak locally. (mk and ak are never sent to the server).

Registration Steps

  1. Client chooses default for pw_cost (minimum 100,000).
  2. Client generates pw_nonce:

    pw_nonce = random_string(256)
    
  3. Client computes pw, mk, and ak using step (3) from Login Steps.

  4. Client registers with email, pw, pw_cost, pw_nonce, and version.

Item Encryption

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:

  • The item's content.
  • The item's enc_item_key.

Client-side

An item is encrypted using a random key generated for each item.

Encryption:

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:

  1. Generate a random 512 bit key item_key (in hex format).
  2. Split item_key in half; set item encryption key item_ek = first_half and item authentication key item_ak = second_half.
  3. Encrypt content using item_ek and item_ak following the instructions "Encrypting a string using the 003 scheme" below and send to server as content.
  4. Encrypt item_key using the global mk and global ak following the instructions "Encrypting a string using the 003 scheme" below and send to server as enc_item_key.

Decryption:

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:

  1. Decrypt enc_item_key using the global mk and global ak according to the "Decrypting a string using the 003 scheme" instructions below to get item_key.
  2. Split item_key in half; set encryption key item_ek = first_half and authentication key item_ak = second_half.
  3. Decrypt content using item_ek and item_ak according to the "Decrypting a string using the 003 scheme" instructions below.

Encrypting a string using the 003 scheme:

Given a string_to_encrypt, an encryption_key, and an auth_key:

  1. Generate a random 128 bit string called IV.
  2. Encrypt string_to_encrypt using AES-CBC-256:Base64, encryption_key, and IV:
  ciphertext = AES-Encrypt(string_to_encrypt, encryption_key, IV)
  1. Generate string_to_auth by 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(":")
  1. Compute auth_hash = HMAC-SHA256:Hex(string_to_auth, auth_key).
  2. Generate the final result by combining the five components into a : separated string: result = ["003", auth_hash, uuid, IV, ciphertext].join(":")

Decrypting a string using the 003 scheme:

Given a string_to_decrypt, an encryption_key, and an auth_key:

  1. Split the string into its constituent parts: components = string_to_decrypt.split(":").
  2. Assign local variables:
  version = components[0]
  auth_hash = components[1]
  uuid = components[2]
  IV = components[3]
  ciphertext = components[4]
  1. Ensure that uuid == item.uuid. If not, abort decryption.
  2. Generate string_to_auth = [version, uuid, IV, ciphertext].join(":").
  3. Compute local_auth_hash = HMAC-SHA256(string_to_auth, auth_key). Compare local_auth_hash to auth_hash. If they are not the same, skip decrypting this item, as this indicates that the string has been tampered with.
  4. Decrypt ciphertext to get final result: result = AES-Decrypt(ciphertext, encryption_key, IV).

Server-side

For every received item:

  1. (Optional but recommended) Encrypt content using server known key and store. Decrypt before sending back to client.

Next Steps

Join the Slack group to discuss implementation details and ask any questions you may have.

You can also email help@standardnotes.org.

Follow @standardnotes for updates and announcements.