アンカーでソラナに暗号化された電子メールサービスを構築するガイド
52488 ワード
プロジェクト概要
私たちはソラナBlockchainのエンドツーエンドの暗号化された電子メールサービスを構築します.このプログラムがどのように機能しているかをまとめましょう.
ユーザーは私たちのdappに自分自身を登録し、彼はDiffie Hellmannキーペア(プライベートと公開鍵)を取得します.公開鍵だけが彼の新しく作成されたアカウントにブロックされている.
そして今、彼は誰か(また、登録されている)に暗号化された電子メールを送ることができます.一旦電子メールが送られるならば、両方の党は彼らの秘密鍵でローカルに電子メールを解読することができます.
ブロックは、暗号化に関する公開情報を保存するために使用されます
iv
and salt
, 例えば、ユーザを登録する.AES 256ビットをカウンタモードで使用します.鍵交換のための楕円曲線Diffie‐Hellmann
必要条件
少なくともさびの基本的な理解、以下のようにインストールします.
構成
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であなたの財布を使用してください.また、質問があれば私にメッセージを送ってください.
Reference
この問題について(アンカーでソラナに暗号化された電子メールサービスを構築するガイド), 我々は、より多くの情報をここで見つけました https://dev.to/gabrieldemian/a-guide-to-build-an-encrypted-email-service-on-solana-with-anchor-2ci7テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol