Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 2, 2022 11:20 pm GMT

A guide to build an encrypted email service on Solana with Anchor

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
  2. Anchor framework

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 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:

[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:

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:

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.

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:

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:

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:

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 and here.

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:

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:

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:

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.

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("
"); console.log("emails: ", emails); console.log("
"); console.log('plaintext: ', plaintext.toString(enc.Utf8)) console.log("shared_secret: ", sharedSecret); console.log("cyphertext: ", emails[0].account.body); console.log("
"); 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:

[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. This is my finished frontend/dapp 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.


Original Link: https://dev.to/gabrieldemian/a-guide-to-build-an-encrypted-email-service-on-solana-with-anchor-2ci7

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To