A guide to build an encrypted email service on Solana with Anchor
Tags: solana, web3, blockchain, tutorial
Published: 2022-04-02 23:20:34

## Project overview
We will be building an end-to-end encrypted email service on the Solana blockchain. Let me give you a summary of how this program works:

The user registers himself on our dapp, he gets a Diffie-Hellmann key pair (a private and public key). Only the public key is stored on his newly created account on the blockchain.

And now, he can send encrypted emails to someone (who is also registered). Once the email is sent, both parties can decrypt the email locally with their private keys, this is secure because the encryption and decryption process is done locally, on the client, without any network request.

The blockchain is only used to store public information about the encryption, like the `iv` and `salt`, for example, and to register users.

We will use AES 256 bits with counter mode as our encryption algorithm. And elliptic curve Diffie-Hellmann for the key exchange.

## Prerequisites
At least a basic understanding of Rust, and the following installed:

1. [Solana CLI](https://docs.solana.com/cli/install-solana-cli-tools)
2. [Anchor framework](https://project-serum.github.io/anchor/getting-started/installation.html)

Anchor is a framework for Solana that makes our life much easier. It handles a lot of dirty work for us. Without it, we would have to do a lot of tedious things, like manual serialization and deserialization.

## Configuration
Make sure that you generated a [development keypair](https://docs.solana.com/wallet-guide/paper-wallet#seed-phrase-generation) on the Solana CLI. And that it has enough SOL to pay for fees. Change the current network to the devnet `solana config set --url devnet` and airdrop some SOL with `solana airdrop 2`.

Now, let's create our project, open up your terminal and paste this command `anchor init encrypted-mail`. "encrypted-mail" is the name of the project, you can change this to any other name.

We need to install a few dependencies on the program, open the `programs/encrypted-mail/Cargo.toml` file, and append the following to the file:

```toml
[dependencies]
uuid = { version = "0.8.*", features = ["serde", "v5"] }
anchor-lang = "0.22.1"
```

Open your program folder `programs/encrypted-mail/src` and add a few files to make your structure be exactly the same as this architecture:
```
...
├─ src
│  ├─ context.rs -> contexts of instructions
│  ├─ error.rs -> error structs
│  ├─ lib.rs -> contains all the instructions
│  ├─ state.rs -> state structs
│  ├─ utils.rs -> helpers functions
...
```

## Coding
Now that we have our foundation, let's start to do some code.

### State.rs
Open your `state.rs` and paste the following:

```rust
use anchor_lang::prelude::*;

#[account]
pub struct Mail {
    pub from: Pubkey,
    pub to: Pubkey,
    pub id: String,
    pub subject: String,
    /* encrypted text */
    pub body: String,
    pub authority: Pubkey,
    pub created_at: u32,
    /* public information about encryption and decryption */
    pub iv: String,
    pub salt: String,
}

#[account]
pub struct UserAccount {
    /* pubkey from diffie helman exchange */
    pub diffie_pubkey: String,
    pub authority: Pubkey,
    pub bump: u8,
}

/* this event allows us to notify clients */
/* when a new email is created */
#[event]
pub struct NewEmailEvent {
    pub from: Pubkey,
    pub to: Pubkey,
    pub id: String,
}
```

Solana stores data in accounts, and those structs are basically the types of our accounts, except for `NewEmailEvent`.

The `iv` and `salt` are generated when the client encrypt data. And it is also used to decrypt it, they are not sensitive data so we can store them on the blockchain without fear.

### Context.rs
Open your `context.rs` file and paste the following:
```rust
use crate::state::{Mail, UserAccount};
use anchor_lang::prelude::*;

#[derive(Accounts)]
pub struct SendMail<'info> {
    #[account(
        init,
        payer = authority,
        space =
            8 +       // discriminator
            32 +      // from
            32 +      // to
            34 +      // id
            40 +      // subject
            512 +     // body
            32 +      // authority
            4 +       // created_at
            20 +      // salt
            36        // iv
    )]
    pub mail: Account<'info, Mail>,
    pub system_program: Program<'info, System>,
    #[account(mut)]
    pub authority: Signer<'info>,
}

#[derive(Accounts)]
pub struct Register<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(
        init,
        payer = authority,
        space =
            8  +          // discriminator
            4  + 64 +     // public key
            32 +          // authority
            1,            // bump
       seeds = [b"user-account", authority.key().as_ref()],
       bump
    )]
    pub user_account: Account<'info, UserAccount>,
    pub system_program: Program<'info, System>,
}
/* helper table for calculating accounts spaces */
/*
    bool	        1 byte	    1 bit rounded up to 1 byte.
    u8 or i8	    1 byte
    u16 or i16	    2 bytes
    u32 or i32	    4 bytes
    u64 or i64	    8 bytes
    u128 or i128	16 bytes
    [u16; 32]	    64 bytes	32 items x 2 bytes. [itemSize; arrayLength]
    PubKey	        32 bytes	Same as [u8; 32]
    vec<u16>	    Any multiple of 2 bytes + 4 bytes for the prefix	Need to allocate the maximum amount of item that could be required.
    String	        Any multiple of 1 byte + 4 bytes for the prefix	Same as vec<u8>
*/
```

Those structs are the **context** of the instructions. They hold and manage all the accounts that the instruction will interact.

An **account** will be managed by the `#[account()]` macro, you declare if the account is mutable or not, if it is a new account to be initialized with x amount of space, the constraints that it must obey etc. Please read the official documentation about this [here](https://book.anchor-lang.com/chapter_3/the_accounts_struct.html).

An **instruction** is just a normal function that will be called on the client to interact with the program.

We have 2 instructions: `register` and `send_email`. 

### Error.rs
Open your `error.rs` file and paste the following:

```rs
use anchor_lang::prelude::*;

#[error_code]
pub enum ErrorCode {
    #[msg("Invalid instruction")]
    InvalidInstruction,

    #[msg("The body of your email is too long. The max is 512 chars")]
    InvalidBody,

    #[msg("The subject of your email is too long. The max is 40 chars")]
    InvalidSubject,

    #[msg("The salt should be exactly 16 chars")]
    InvalidSalt,

    #[msg("The IV should be exactly 32 chars")]
    InvalidIv,

    #[msg("The diffie publickey should be exactly 64 chars")]
    InvalidDiffie,
}
```

This simply maps an error to a message.

### Utils.rs
Open your `utils.rs` and paste the following:

```rs
use anchor_lang::prelude::Pubkey;
use uuid::Uuid;

/* creates a unique ID for a mail using now, body, and sender as arguments */
pub fn get_uuid(now: &u32, body: &String, sender: &Pubkey) -> String {
    const V5NAMESPACE: &Uuid = &Uuid::from_bytes([
        16, 92, 30, 120, 224, 152, 10, 207, 140, 56, 246, 228, 206, 99, 196, 138,
    ]);

    let now = now.to_be_bytes();
    let body = body.as_bytes();
    let sender = sender.to_bytes();

    let mut vec = vec![];

    vec.extend_from_slice(&now);
    vec.extend_from_slice(&body);
    vec.extend_from_slice(&sender);

    Uuid::new_v5(V5NAMESPACE, &vec).to_string()
}
```

We have only one helper function, the `get_uuid`, this will take a few arguments and generate a unique id for each email.

### Lib.rs
Open your `lib.rs` file and paste the following:

```rs
use {crate::error::ErrorCode, anchor_lang::prelude::*, context::*, utils::*};
pub mod context;
pub mod error;
pub mod state;
pub mod utils;

declare_id!("9KVS65SWuX5jnmJkzpyXMCdeKpad9G5sSoKopUUgDiA");

#[program]
pub mod encrypted-mail {
    use super::*;
    use anchor_lang::Key;
    
    pub fn send_email(
        ctx: Context<SendMail>,
        subject: String,
        body: String,
        from: Pubkey,
        to: Pubkey,
        salt: String,
        iv: String
    ) -> Result<()> {
        require!(subject.chars().count() < 50, ErrorCode::InvalidSubject);
        require!(body.chars().count() < 280, ErrorCode::InvalidBody);
        require!(salt.chars().count() == 16, ErrorCode::InvalidSalt);
        require!(iv.chars().count() == 32, ErrorCode::InvalidIv);

        let now = Clock::get().unwrap().unix_timestamp as u32;
        let mail = &mut ctx.accounts.mail;
        let id = get_uuid(&now, &body, &mail.key());

        mail.from = from;
        mail.to = to;
        mail.id = id.clone();
        mail.subject = subject;
        mail.body = body; // encrypted body, a ciphertext
        mail.created_at = now;
        mail.salt = salt;
        mail.iv = iv;
        mail.authority = *ctx.accounts.authority.key;

        emit!(state::NewEmailEvent {
            from,
            to,
            id
        });

        Ok(())
    }

pub fn register(ctx: Context<Register>, diffie_pubkey: String) -> Result<()> {
        require!(diffie_pubkey.chars().count() == 64, ErrorCode::InvalidDiffie);

        let user_account = &mut ctx.accounts.user_account;

        user_account.diffie_pubkey = diffie_pubkey;
        user_account.authority = *ctx.accounts.authority.key;
        user_account.bump = *ctx.bumps.get("user_account").unwrap();

        Ok(())
    }
}
```

You need to change `declare_id!` with your account id, Anchor will output this information when you run `anchor build`.

The `require!` ensures that the user passes the right data to the instructions. If the condition is false, it will return an error.

The `*ctx.bump.get("user_account")` is an abstraction for generating a bump seed for this PDA, please read about PDA's [here](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/#program-derived-addresses-pdas-part-1) and [here](https://book.anchor-lang.com/chapter_3/PDAs.html).

Now, that the program itself is done, we just need to make some tests on the client.

### Testing
Add a few dependencies with this command: `yarn add crypto-js elliptic text-encoding`

Let's create a file `/utils.ts` at the root of the project, this file will contain abstractions to make readability easier on our tests. Paste the following into the file:

```ts
import { PublicKey } from "@solana/web3.js";
import { TextEncoder } from "text-encoding";
import idl from "./target/idl/minerva.json";
import { getProvider } from "@project-serum/anchor";
import { ec } from 'elliptic'

export const DEVNET_WALLET = getProvider().wallet.publicKey;

export const getUserPDA = async (
  seed: string,
  authority: PublicKey = DEVNET_WALLET
) => {
  const [PDA] = await PublicKey.findProgramAddress(
    [new TextEncoder().encode(seed), authority.toBuffer()],
    new PublicKey(idl.metadata.address)
  );
  return PDA;
};

export const elliptic = new ec('curve25519')
```

The `getUserPDA` will generate a PDA for the user when he registers. We need to pass an array of seeds to generate the address.

The `elliptic` is just an instance of the curve25519 class that we imported from the `elliptic` library.

Open `/tests/encrypted-mail.ts` and let's start testing. You can erase everything and paste the following code:

```ts
import {
  Program,
  workspace,
  Provider,
  setProvider,
} from "@project-serum/anchor";
import AES from 'crypto-js/aes'
import { enc, mode, lib } from 'crypto-js'
import { Keypair, SystemProgram } from "@solana/web3.js";
import { EncryptedMail } from "../target/types/encrypted-mail";
import { DEVNET_WALLET, getUserPDA, elliptic } from "../utils";
import { expect } from "chai";

describe("beggining encrypted-mail tests", () => {
  setProvider(Provider.env());

  /* generating diffie helmann keys */
  const aliceKeypair = elliptic.genKeyPair()
  const bobKeypair = elliptic.genKeyPair()

  const aliceDiffiePublic = aliceKeypair.getPublic().encode("hex", true)
  const bobDiffiePublic = bobKeypair.getPublic().encode("hex", true)

  const sharedSecret = aliceKeypair.derive(bobKeypair.getPublic()).toString("hex")

  /* generating blockchain wallets */
  const alice = DEVNET_WALLET;
  const bob = Keypair.generate();

  const program = workspace.EncryptedMail as Program<EncryptedMail>;
}
```

We generate 2 key pairs, one for Alice, and one for Bob. We can generate a shared private key from the private of one of them, and the public of the other. It is the shared private that will be used to encrypt and decrypt the emails.

Let's add our first test, append the following code inside your `describe` function:

```ts
it("can register alice and bob", async () => {
    const aliceAccountPDA = await getUserPDA("user-account");
    const bobAccountPDA = await getUserPDA("user-account", bob.publicKey);

    const airdropTx = await program.provider.connection.requestAirdrop(
      bob.publicKey,
      1000000000
    );

    await program.provider.connection.confirmTransaction(airdropTx);

    await program.rpc.register(aliceDiffiePublic, {
      accounts: {
        authority: alice,
        userAccount: aliceAccountPDA,
        systemProgram: SystemProgram.programId,
      },
    });

    await program.rpc.register(bobDiffiePublic, {
      accounts: {
        authority: bob.publicKey,
        userAccount: bobAccountPDA,
        systemProgram: SystemProgram.programId,
      },
      signers: [bob]
    });

    const users = await program.account.userAccount.all();

    console.log("users: ", users);
    expect(users.length).to.equal(2);
  });
```

This is very straightforward, the function start by generating the PDA for Alice and Bob and then calls the `register` instruction from the program, and then we get all the userAccounts and check to see if they equal 2.

Finally, the last test is to encrypt the email, send the email, and decrypt it back.

```ts
it("can encrypt emails, send the emails, and decrypt it", async () => {
    const mailA = Keypair.generate();

    let cipher = AES.encrypt("simplesmente intankavel o bostil", sharedSecret, { mode: mode.CTR })

    await program.rpc.sendEmail(
      "very important subject", // subject
      cipher.ciphertext.toString(), // body of email
      alice, // from
      bob.publicKey, // to
      cipher.salt.toString(), // salt
      cipher.iv.toString(), // iv
      {
        accounts: {
          authority: alice,
          mail: mailA.publicKey,
          systemProgram: SystemProgram.programId,
        },
        signers: [mailA],
      }
    );

    const emails = await program.account.mail.all();

    const email = emails[0].account

    const plaintext = AES.decrypt(
      {
        ciphertext: enc.Hex.parse(email.body),
        iv: enc.Hex.parse(email.iv),
        salt: enc.Hex.parse(email.salt)
      } as lib.CipherParams,
      sharedSecret,
      { mode: mode.CTR }
    )

    console.log("\n");
    console.log("emails: ", emails);
    console.log("\n");
    console.log('plaintext: ', plaintext.toString(enc.Utf8))
    console.log("shared_secret: ", sharedSecret);
    console.log("cyphertext: ", emails[0].account.body);
    console.log("\n");

    expect(plaintext.toString(enc.Utf8)).to.equal('simplesmente intankavel o bostil');
  });
```

First, we generate an email account, then encrypt the body of the email, and call the `sendEmail` instruction.

After that, we get the email back and decrypt the body of the email. And check to see if the decrypted message is the same message that was encrypted.

To run the tests, first, you need to build the program with `anchor build`. At the end of the build, Anchor will print your program id on your terminal, you need to copy this and replace on `declare_id!` macro on `lib.rs` and also on `/Anchor.toml`. This file should look like this:

```toml
[programs.localnet]
encrypted-mail = "9KVS65SWuX5jnmJkzpyXMCdeKpad9G5sSoKopUUgDiA"

[programs.devnet]
encrypted-mail = "9KVS65SWuX5jnmJkzpyXMCdeKpad9G5sSoKopUUgDiA"

[registry]
url = "https://anchor.projectserum.com"

[provider]
cluster = "localnet"
wallet = "~/.config/solana/devnet.json"

[scripts]
test = "npx ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
```
Now you can run `anchor deploy` and `anchor test`. And that's it!

## The end
I could not dive into every technical aspect of Solana in detail, because there would be just too much to be explained here, I prefer to focus on a more practical approach at first, you should read the official documentation of Anchor and Solana to understand better the theory behind all of this.

If you made it this far, congratulations! If you got lost at some point, you can check the [source code here](https://github.com/gabrieldemian/minerva-contract). This is my finished [frontend/dapp](https://minerva-front-omega.vercel.app/) of this program, make sure you use your wallet on the **devnet**. Also, feel free to send me a message if you have any questions.