This blog is a work in progress.

writing.

Encrypt and decrypt data with the "Orion" library in Rust

Cover Image for Encrypt and decrypt data with the "Orion" library in Rust
Anthony Manikhouth
Anthony Manikhouth

Storing sensitive data? Why?

At Symbiose, we're building an awesome app that will interact with multiple blockchains, thus requiring some reflexion about one of the core element of a blockchain, the wallet and specificaly its storing logic.

Let's take the example of the Solana blockchain. A Solana wallet is represented by a Keypair object, which is a pair of public and private keys.

solana-cli is a command line interface for the Solana blockchain, that allows you to generate a new wallet, or to import a wallet from a file. It uses simple JSON files to store the generated keypairs, saving the unencrypted private key as a u8 array in the file. To retrieve it back, the cli derives the public key from the private key and then uses Keypair::from_bytes to create a new Keypair object.

While this method is simple and fast, solana-cli stores this data as plaintext and anyone gaining access to the machine could easily fetch the keypairs.

# A key is often generated like this... solana-keygen new --outfile ~/.config/solana/keypairs/default.json # ... And an attacker could easily reads its contents like this. cat ~/.config/solana/keypairs/default.json

So what can we do to prevent this?

A solution is to encrypt the private key with an AEAD algorithm and store it wherever you want.

One of the safest way to do this today is to use the ChaCha20 and Poly1305 algorithms (RFC8439).

To achieve this, we will need to use the orion library which offers a good aead module using XChaCha20Poly1305.

Encrypting data with the orion library

First of all, a Secret Key is needed to encrypt the data. This key will be generated by deriving a combinaison of our plaintext password and a random salt.

Let's start by creating a new orion::kdf::Password object.

use orion::hazardous::stream::chacha20::CHACHA_KEYSIZE; use orion::kdf::{derive_key, Password, Salt}; let password = Password::from_slice(password.as_bytes()).with_context(|| "Password error")?;

Then, we can derive the Secret Key from the password and the salt. But how do you get it?

pub fn nonce() -> Result<[u8; 24]> { let mut result = [0u8; 24]; getrandom::getrandom(&mut result)?; Ok(result) }

Yay, now that we have a nonce. Let's derive the secret key.

let salt = Salt::from_slice(salt).with_context(|| "Salt is too short")?; let kdf_key = derive_key(&password, &salt, 15, 1024, CHACHA_KEYSIZE as u32) .with_context(|| "Could not derive key from password")?;

And finally get the Secret Key from the kdf_key object.

let key = SecretKey::from_slice(kdf_key.unprotected_as_bytes()) .with_context(|| "Could not convert key")?;

Nice, we have our Secret Key! Now let's use it to encrypt our data.

These are the struct and functions we need to use to encrypt our data.

use orion::hazardous::{ aead::xchacha20poly1305::{seal, Nonce, SecretKey as XSecretKey}, mac::poly1305::POLY1305_OUTSIZE, stream::xchacha20::XCHACHA_NONCESIZE, };

Let's use the function we just created to encrypt to retrieve a Secret Key object.

let key = get_key_from_password(password, nonce)?; let key = XSecretKey::from_slice(key.unprotected_as_bytes()).with_context(|| "Key is invalid")?;

Alright! Now that we have our Secret Key object, we can prepare the output buffer for the encrypted data.

let nonce = Nonce::from_slice(nonce).with_context(|| "Nonce is too short")?; // Get the output length let output_len = match plaintext.len().checked_add(XCHACHA_NONCESIZE + POLY1305_OUTSIZE) { Some(min_output_len) => min_output_len, None => bail!("Plaintext is too long"), }; // Allocate a buffer for the output let mut output = vec![0u8; output_len]; output[..XCHACHA_NONCESIZE].copy_from_slice(nonce.as_ref());

And finally, store the encrypted data inside output:

seal(&key, &nonce, plaintext, None, &mut output[XCHACHA_NONCESIZE..]) .with_context(|| "Could not convert key")?;

Voilà!

Decrypting data with the orion library

Primarily, let's check that the data is valid.

use orion::aead::open; use orion::hazardous::stream::xchacha20::XCHACHA_NONCESIZE; ensure!(ciphertext.len() > XCHACHA_NONCESIZE, "Ciphertext is too short");

Now get the Secret Key to decrypt the ciphertext.

let key = get_key_from_password(password, &ciphertext[..XCHACHA_NONCESIZE])?; open(&key, ciphertext).with_context(|| "Ciphertext was tampered with")

Applied to Solana

Let's create a wallet with Solana SDK and encode it for future usage.

use solana_sdk::signature::Keypair; let wallet = Keypair::new();

Now that we have the wallet, what you want to do is converting it to plainbytes to prepare it for the encoding. Use the Keypair::to_base58_string() function to convert the structure to a readable string, then transform it to bytes.

let plainbytes = wallet.to_base58_string().as_bytes().to_vec();

Finally, use the defined encrypt function to encrypt the wallet.

let nonce = nonce()?; encrypted_private_key = encrypt(&plainbytes, password, &nonce)?;

The full source code can be found here