Projects

Encrypted sharing filesystem

Symmetric encryption
Asymmetric encryption
PoC
Applied cryptography
Rust
State-of-Art security

PoC of an API server that allows users to create an account, upload encrypted files and directories. They are able to share it to each other. The server never know anything about what is upload as everything is encrypted with user secret.

Introduction


This project aims to develop an encrypted file management system with the possibility of sharing with one or more parties. The server never sees anything, all the content is encrypted and the connection is supposed to be encrypted using TLS 1.3. All the user secret are computed locally in client side.

This project has been full developed in Rust. You can get the code here :

Threat model


In this present case, we only consider active attackers. DoS is not considered.

Choice of algorithms


  • XChaCha20-Poly1305: This choice is justified by the X, which means extended compared to the IV; it is extended to 24 bytes instead of 12. Since the system is an encrypted file server, it is imperative to protect against active attackers, which is why authenticated encryption is chosen. In our case, each entity (file/folder) has a different key; a key cannot be used to encrypt two elements. Therefore, we want one. Furthermore, XChaChaPoly1305 is audited on cryptography.rs.
    • The IV is generated randomly with a CSPRNG, so it is subject to the birthday paradox. But in this implementation, the risk is minimal because the IV is 24 bytes, which means 12 bytes without any collision for a given key (very acceptable for us: 2^96 ciphertexts for one key).
    • Performance: the main calculation operation is XOR, which is not very resource-intensive. Here is a benchmark showing the performance of XChaCha20 compared to AES-GCM (with accelerated instructions). The result is as follows: XChaCha20 is faster without instructions than AES-GCM with instructions.
    • As this algorithm is a stream cipher the nonce never must be repeated. To avoid any issue, for each update of cipher, a new nonce will be randomly generated with a CSPRNG. In additional of that, it cover the case of untrusted filesystem (for exemple a malicious filesystem that backup every file changes).
  • RSA-OAEP: Valid for the future according to Ecrypt, and the RSA crate is audited on cryptography.rs.
    • Used for asymmetric encryption for file sharing between users.
    • The RSA crate is audited on cryptography.rs, and RSA-OAEP is IND-CCA against black-box adversaries, which corresponds to our case.
    • Key size chosen: 4096 bits, recommended for long-term protection according to keylength.com, as 3072 bits for coverage beyond 2030. Given the power of our current chips, I chose 4096 bits.
  • Argon2id: A hybrid version of argon2 that is resistant to side-channel attacks and GPU/FPGA attacks (increases the RAM usage).
    • Used to derive a secure password based on the user’s password.
      • Very popular and soon to replace PBKDF2 because it offers better protection, especially against side-channel attacks and RAM management. Moreover, it has won the competition.
      • Better security analysis than PBKDF2, Bcrypt, and Scrypt (see CAA courses).
      • Uses a base of Blake2 as a compression function, which is efficient according to the graph below.
  • Blake3 hashing function:
    • Faster than blake2
    • Used in several cryptocurrency projects.
    • Can be used KDF, MAC.
    • Every keys in the following architecture will be derived with blake3.
    • Peformance compared to other legacy hashing functions (from blake3 repository):

File system


Creating a folder

There are two possible scenarios:

  1. Creating the folder at the root of the user’s directory: a symmetric folder key of 32 bytes is generated with a CSPRNG, and this key is then encrypted with the user’s master key.
  2. Creating a folder in the hierarchy: a symmetric folder key of 32 bytes is generated with a CSPRNG, and this key is then encrypted with the parent folder’s key. If the folder is at the root, the owner’s master key is used to encrypt the directory’s master key.

The file name is encrypted, and the nonce is generated with a CSPRNG. The clear nonce and the encrypted name are stored in the folder’s metadata on the server.

Creating a file

A file key of 32 bytes is generated with a CSPRNG, and this key is then encrypted with the folder key in which it is located. By default, a root folder is created to avoid directly encrypting a file with the master key.

The file name is encrypted, the file content is encrypted. Nonces are generated with a CSPRNG. The nonces are stored in clear, and the file content is encrypted, all of which are stored in the metadata on the server.

Sharing a folder and file

To share a folder X with user Bob, the owner of X will decrypt the folder key he want to share, either directly with his master key if it’s the root folder or by cascading decryption with his master key for the first folder and then the child folder’s key with the freshly decrypted first folder’s key, and so on until the folder to be shared.

Once folder key X is decrypted, the owner will encrypt this key with Bob’s public key so that Bob can access it by decrypting it with his asymmetric private key.

The same process applies to sharing a file; all you need to do is decrypt the file key with the key of the folder in which it is located and then encrypt it with Bob’s public key.

PS: The same schema applies to sharing a folder because instead of decrypting a file key, it will be a folder key.

File sharing

To share a file only, I use the same process as for sharing a folder. You just need to generate a unique key with a CSPRNG for the created file, so you only need to share this key with the user to access the file and not the entire folder. User X who wants to share the file toto.txt encrypts the key of toto.txt with the public key of user Y. Y only has to decrypt it with his private key.

Untitled

Revocation

In this implementation, revocation is done at the implementation level with authentication. So, when there is a revocation of sharing, it is removed from the list of shares for the user with whom it was shared.

Cryptographically speaking, to revoke a folder, you need to decrypt all children (folders and files), regenerate a key for each one, and then re-encrypt all these keys in cascade with the newly generated key for the given folder.

In the case of revoking a file, you simply need to regenerate a file key for the file and re-encrypt it with its parent’s key.

Finally, if these files/folders were shared with other users, you will need to re-encrypt the new file/folder keys with the public keys of the users with whom they were shared.

Authentication


Creating an account

When creating an account, the user must provide a username and a password. Then, a symmetric key of 32 bytes is generated with argon2id with the following parameters:

let params = Params::new(
        1048576,  // Memory cost
        3,        // Time cost
        1,        // Parallelism
        Some(32), // Salt length
    )
    .unwrap();

I chose these parameters because the symmetric key, which encrypts the master key and the authentication key, is derived from the hashed password. It is therefore imperative that it cannot be brute-forced.

The total account creation time is approximately 5 seconds, including the generation of 4096-bit RSA private keys, which is acceptable for me.

When the user has created his account on the client side, he sends it to the server. The server takes the clear authentication key and passes it through a Blake3 MAC, which is then hashed with a cryptographic pepper. The resulting MAC is stored in the user’s auth_key field.

Untitled

Cryptographic pepper

To prevent brute-forcing of hashed passwords in case of a database user table leak, I passed the authentication key in Blake3 (MAC mode) with a cryptographic pepper as key. So, when a user logs in, the MAC of his authentication key is compared to the one in the database. If the database were to leak, it would still require knowledge of the pepper value to calculate the MAC output. This makes it impossible to brute force.

Login

Untitled

When a user already has an account, he send his username to the server, which then sends him his salt in clear text. The client calculates its different keys and sends the authentication key to the server. The server passes it through the Blake3 MAC function authenticated with the pepper and compares it with the stored MAC (user’s authentication key). If it is valid, the server sends back a JWT.

Detailed key generation


Untitled

Changing password

To do this, the connected user must recompute his authentication key and symmetric key for his current password. With the symmetric key, he decrypts his master key. Then, he generates a new symmetric and authentication key based on the new password. The new symmetric key re-encrypts the master key. Finally, the client sends the updated data (master key, authentication key) with the current authentication key, which allows verification if the user is legitimate for this password change. If yes, the new authentication key is passed through the Blake3 MAC hashed with the pepper and stored in the user’s metadata.

Implementation


The server never sees plaintext data. Each data sent to the server is encoded in base58, allowing any value to be represented in strictly alphanumeric form. This is useful for creating the file hierarchy in the file system. Each encrypted file name is represented in base58, making it easy to create a file with a valid name without checking for special characters. The same applies to folders. When I create a file: path/to/the/file.txt, on the server side, there will be base58(path encrypted)/base58(to encrypted)/base58(file.txt encrypted). Moreover, this encoding is used in Bitcoin addresses, so it is not an encoding invented from scratch.

In addition, the following features have been implemented:

  1. Account creation
  2. Adding folders/files
  3. Sharing folders/files
  4. Changing the password
  5. Read-only access to folders/files
  6. Revocation of shared folders/files
  7. TLS 1.3 client/server connection
  8. Adding cryptographic pepper on the server side

Server side


Encrypted data stored on the server:

  • For a folder/file:

    pub struct FsEntity {
        pub uid: String, 
        pub path: String, // encrypted
        pub parent_id: String,
        pub name: DataAsset, // encrypted
        pub entity_type: String,
        pub key: DataAsset, // encrypted
        #[serde(skip_serializing_if = "Option::is_none")]
        pub content: Option<DataAsset>, // only used for data transfer, encrypted
    }
    
  • For a user:

    pub struct User {
        pub uid: String,
        pub username: String,
        pub clear_salt: String,
        pub master_key: DataAsset, // encrypted
        pub auth_key: String, // Signed with Blake3 MAC with the pepper as the key
        pub public_key: String, 
        pub private_key: DataAsset, // encrypted
        // contains the file/folder
        pub shared_to_others: Option<Vec<Sharing>>, // encrypted key
        pub shared_to_me: Option<Vec<Sharing>>, // encrypted key
    }
    

DataAsset is a struct that contains an optional value and an optional nonce.

TLS

According to Ecrypt, RSA PKCS#1 2.2 is no longer recommended, so I followed Mozilla’s recommendations for TLS configurations, opting for the modern configuration:

Untitled I generated my certificates with ECDSA and the secp384r1 elliptic curve. For the TLS 1.3 configuration, for compatibility reasons with the CLI client, I used the following cipher suites:

  • TLS_CHACHA20_POLY1305_SHA256
  • TLS_AES_256_GCM_SHA384
  • TLS_AES_128_GCM_SHA256
  • TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
  • TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
  • TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  • TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
  • TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

Since this implementation is academic, the root certificate used is self-signed.

To do this, you need to generate 3 certificates → create a PKI:

  1. Root CA

    Configured to sign intermediate certificates.

  2. Intermediate CA

    Configured to sign application or service certificates, such as a website or an API.

  3. API Vault crypto

    Configured for digital signatures and client authentication. It cannot sign other certificates.

Finally, it is important to concatenate the intermediate certificate with the API service certificate and provide the API service certificate’s private key to sign communications between the client and the API.

Avoid MITM attack

To avoid MITM attacks, a good practice would be to hard-code the certificate in the binary to be executed. In this case, the only way to deal with the attack would be to reverse-engineer the client application and change the certificate in the code. This would be very difficult, especially in Rust.