アンカーでソラナに暗号化された電子メールサービスを構築するガイド


プロジェクト概要


私たちはソラナBlockchainのエンドツーエンドの暗号化された電子メールサービスを構築します.このプログラムがどのように機能しているかをまとめましょう.
ユーザーは私たちのdappに自分自身を登録し、彼はDiffie Hellmannキーペア(プライベートと公開鍵)を取得します.公開鍵だけが彼の新しく作成されたアカウントにブロックされている.
そして今、彼は誰か(また、登録されている)に暗号化された電子メールを送ることができます.一旦電子メールが送られるならば、両方の党は彼らの秘密鍵でローカルに電子メールを解読することができます.
ブロックは、暗号化に関する公開情報を保存するために使用されますiv and salt , 例えば、ユーザを登録する.
AES 256ビットをカウンタモードで使用します.鍵交換のための楕円曲線Diffie‐Hellmann

必要条件


少なくともさびの基本的な理解、以下のようにインストールします.
  • Solana CLI
  • Anchor framework
  • アンカーは、我々の人生をより簡単にするソラナのためのフレームワークです.それは、我々のために多くの汚い仕事を扱います.それがなければ、マニュアルのシリアル化と逆シリアル化のような退屈なものをたくさんしなければならないでしょう.

    構成


    Aを生成したことを確認しますdevelopment keypair ソラナCLIについてそして、それは手数料の支払いに十分なソルを持っている.現在のネットワークをdevnetに変更するsolana config set --url devnet そして、いくつかのゾルをairdropsolana airdrop 2 .
    プロジェクトを作成し、端末を開いてこのコマンドをペーストしましょうanchor init encrypted-mail . 「暗号化メール」はプロジェクトの名前です.
    プログラムにいくつかの依存関係をインストールする必要がありますprograms/encrypted-mail/Cargo.toml ファイルに次のように追加します.
    [dependencies]
    uuid = { version = "0.8.*", features = ["serde", "v5"] }
    anchor-lang = "0.22.1"
    
    プログラムフォルダを開くprograms/encrypted-mail/src そして、あなたの構造をこのアーキテクチャと全く同じにするためにいくつかのファイルを加えてください.
    ...
    ├─ src
    │  ├─ context.rs -> contexts of instructions
    │  ├─ error.rs -> error structs
    │  ├─ lib.rs -> contains all the instructions
    │  ├─ state.rs -> state structs
    │  ├─ utils.rs -> helpers functions
    ...
    

    コーディング


    今私たちの基礎を持って、いくつかのコードを開始しましょう.

    状態。RS


    オープンユアstate.rs ペーストします.
    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,
    }
    
    ソラナは口座にデータを格納します、そして、それらの構造は基本的に我々の口座のタイプですNewEmailEvent .
    The iv and salt クライアントがデータを暗号化する際に生成されます.また、それは復号化するために使用され、彼らは敏感なデータではないので、我々はブロックせずに恐怖なく保存することができます.

    コンテキスト.RS


    オープンユアcontext.rs ファイルをペーストします.
    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>
    */
    
    これらの構造体は命令のコンテキストです.彼らは、命令が相互に作用するすべてのアカウントを保持して、管理します.
    アカウントは、#[account()] マクロは、アカウントが変更可能であるかどうかを宣言します.もし、新しいアカウントがX量のスペースで初期化される場合、それに従う必要がありますhere .
    命令は、プログラムと対話するためにクライアントで呼ばれる通常の機能です.
    2つの手順があります.register and send_email .

    エラーです。RS


    オープンユアerror.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,
    }
    
    これは単にエラーをメッセージにマップします.

    UtilsRS


    オープンユアutils.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()
    }
    
    私たちには、1つのヘルパー機能がありますget_uuid , これは、いくつかの引数を取り、各メールの一意のIDを生成します.

    リブ.RS


    オープンユアlib.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(())
        }
    }
    
    変更する必要がありますdeclare_id! あなたのアカウントIDで、あなたが走るとき、アンカーはこの情報を出力しますanchor build .
    The require! ユーザーが正しいデータを指示に渡すことを保証します.条件がfalseの場合、エラーが返されます.
    The *ctx.bump.get("user_account") このPDAのためのバンプシードを生成するための抽象化は、PDAhere and here .
    さて、プログラム自体が完了すると、クライアントにテストを行う必要があります.

    テスト


    このコマンドでいくつかの依存関係を追加します.yarn add crypto-js elliptic text-encodingファイルを作りましょう/utils.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 ユーザが登録するときにPDAを生成します.アドレスを生成するために種の配列を渡す必要があります.
    The elliptic ちょうど私たちが2009年から輸入したCurve 25519クラスのインスタンスですelliptic 図書館.
    オープン/tests/encrypted-mail.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>;
    }
    
    私たちは2つのキーペア、1つのアリスを生成し、1つのボブ.私たちは、それらのうちの1つの個人からの共有秘密鍵を生成することができます.これは、暗号化し、電子メールを復号化するために使用される共有プライベートです.
    最初のテストを加えましょうdescribe 機能
    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);
      });
    
    これは非常に簡単です、関数はアリスとボブのためにPDAを生成することによって起動しregister プログラムからの指示、そして我々はすべてのユーザーアカウントを取得し、彼らが2に等しいかどうかを確認してください.
    最後に、最後のテストは、電子メールを暗号化し、メールを送信し、それを解読することです.
    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');
      });
    
    まず、メールアカウントを生成し、メールの本文を暗号化し、sendEmail 指示.
    その後、私たちは戻って電子メールを取得し、電子メールの本文を復号化します.そして、復号化されたメッセージが暗号化されたのと同じメッセージかどうかチェックしてください.
    テストを実行するには、まずプログラムをビルドする必要がありますanchor build . ビルドの終了時に、アンカーはプログラムIDを端末に出力しますdeclare_id! マクロオンlib.rs そして/Anchor.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"
    
    今すぐ実行することができますanchor deploy and anchor test . そして、それ!

    終わり


    私は詳細にソラナのすべての技術的な側面に飛び込むことができませんでした、ここで説明するにはあまりに多くがあるので、私は最初により実用的なアプローチに集中するのを好みます.
    あなたがこれまでそれを作ったならば、おめでとう!いくつかの時点で失われた場合は、チェックすることができますsource code here . これが終わりですfrontend/dapp このプログラムの場合は、devnetであなたの財布を使用してください.また、質問があれば私にメッセージを送ってください.