diff --git a/Cargo.lock b/Cargo.lock index ee73c6b..f1b767f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "aes" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ctutils", + "ghash", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -114,6 +149,7 @@ dependencies = [ name = "arch-kit" version = "0.1.6" dependencies = [ + "aes-gcm", "apl-associated-token-account", "apl-token", "arch-satellite-lang-idl", @@ -124,11 +160,13 @@ dependencies = [ "clap", "flate2", "hex", + "hkdf", "include_dir", "minijinja", "rand 0.8.7", "reqwest", "serde_json", + "sha2", "tempfile", "thiserror 2.0.20", ] @@ -498,6 +536,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "borsh" version = "1.8.0" @@ -596,6 +643,17 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout", +] + [[package]] name = "clap" version = "4.6.6" @@ -636,6 +694,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "colorchoice" version = "1.0.5" @@ -681,6 +745,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -718,6 +788,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -756,8 +853,9 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", ] [[package]] @@ -988,6 +1086,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", +] + [[package]] name = "glam" version = "0.33.5" @@ -1055,6 +1162,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.5.0" @@ -1094,6 +1219,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.11.0" @@ -1318,6 +1452,15 @@ dependencies = [ "web-time", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ipnet" version = "2.12.1" @@ -1628,6 +1771,17 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "polyval" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.15.0" @@ -2663,6 +2817,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index a520772..dc3e752 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ include = [ ] [dependencies] +aes-gcm = { version = "0.11.1", default-features = false, features = ["aes", "alloc"] } apl-associated-token-account = { version = "=0.10.0", features = ["no-entrypoint"] } apl-token = { version = "=0.10.0", features = ["no-entrypoint"] } arch_sdk = "=0.10.0" @@ -29,11 +30,13 @@ bs58 = "0.5" clap = { version = "4.5", features = ["derive", "env"] } flate2 = "1.1" hex = "0.4" +hkdf = "0.12" include_dir = "0.7" minijinja = "2" rand = "0.8" reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } serde_json = "1.0" +sha2 = "0.10" thiserror = "2.0" [dev-dependencies] diff --git a/README.md b/README.md index fefdeb2..5925129 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ Install it with `rustup toolchain install nightly`; Satellite's | --- | --- | --- | | [`keygen`](#generate-keys) | `arch-kit keygen [OPTIONS] ...` | Generate one or more secp256k1 key files, with optional public key prefixes (vanity). | | [`pubkey`](#derive-a-public-key) | `arch-kit pubkey ` | Derive a Base58 Arch public key from a secret key file. | +| [`encrypt`](#encrypt-and-decrypt-messages) | `arch-kit encrypt --key [TEXT]` | Encrypt a UTF-8 message with AES-256-GCM. | +| [`decrypt`](#encrypt-and-decrypt-messages) | `arch-kit decrypt --key [TEXT]` | Decrypt a message using the same secret key file. | ### Token program @@ -245,6 +247,33 @@ arch-kit pubkey ./keys/authority.key The command reads either supported secret-key file format and writes only the derived Base58 Arch public key to standard output. +## Encrypt and decrypt messages + +```bash +arch-kit encrypt --key ./keys/authority.key "Hello" +arch-kit decrypt --key ./keys/authority.key "" + +# Omit TEXT to read from stdin: +printf 'Hello' | arch-kit encrypt --key ./keys/authority.key > message.enc +arch-kit decrypt --key ./keys/authority.key < message.enc +``` + +Both commands run locally and accept existing hex or SDK JSON secret-key files. +Encryption and decryption require the same private key. Messages must be UTF-8; +empty messages, Unicode, and multiline text are supported. Encryption prints a +Base64 payload with a trailing newline. Decryption preserves the original text +exactly without adding a newline. Add `--json` for `{"ciphertext":"..."}` or +`{"message":"..."}` output. Authentication failures exit unsuccessfully without +printing plaintext. + +Encryption uses [RustCrypto AES-GCM](https://docs.rs/aes-gcm/), whose documentation +reports an NCC Group audit. A dedicated 32-byte AES key is derived from the raw +32-byte private key using HKDF-SHA256, no salt, and the context +`arch-kit/message-encryption/v1`. Each message uses a fresh OS-random 12-byte +nonce and a full 16-byte authentication tag. The format is standard padded Base64 +of `0x01 || nonce || ciphertext || tag`; the version byte is authenticated as +associated data. Surrounding whitespace in an encrypted payload is ignored. + ## Inspect tokens Derive an ATA locally without contacting an RPC node: diff --git a/src/cli.rs b/src/cli.rs index 5d517eb..2715a4c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,9 +2,9 @@ use clap::{Parser, Subcommand}; use crate::{ commands::{ - arch_balance, ata, build_idl, create_mint, deploy, faucet, init, keygen, mint_info, - mint_tokens, pubkey, token_account, token_accounts, token_balance, token_transfer, - transfer_arch, + arch_balance, ata, build_idl, create_mint, deploy, encryption, faucet, init, keygen, + mint_info, mint_tokens, pubkey, token_account, token_accounts, token_balance, + token_transfer, transfer_arch, }, network::{BitcoinNetwork, DEFAULT_RPC_URL}, }; @@ -58,6 +58,12 @@ pub(crate) enum Command { /// Derive an Arch public key from a secret key file. Pubkey(pubkey::Args), + /// Encrypt a UTF-8 message using a secret key file and AES-256-GCM. + Encrypt(encryption::Args), + + /// Decrypt an AES-256-GCM message using the same secret key file. + Decrypt(encryption::Args), + /// Derive an associated token account address for an owner and mint. Ata(ata::Args), diff --git a/src/commands/encryption.rs b/src/commands/encryption.rs new file mode 100644 index 0000000..af0d90c --- /dev/null +++ b/src/commands/encryption.rs @@ -0,0 +1,323 @@ +use std::{ + io::{self, Read, Write}, + path::{Path, PathBuf}, +}; + +use aes_gcm::{ + Aes256Gcm, KeyInit, + aead::{Aead, Payload}, +}; +use base64::{Engine, engine::general_purpose::STANDARD}; +use hkdf::Hkdf; +use rand::{RngCore, rngs::OsRng}; +use sha2::Sha256; + +use crate::{ + error::{CliError, Result}, + keys::load_existing_key, +}; + +const VERSION: u8 = 1; +const NONCE_SIZE: usize = 12; +const TAG_SIZE: usize = 16; +const KEY_CONTEXT: &[u8] = b"arch-kit/message-encryption/v1"; + +#[derive(Debug, clap::Args)] +pub(crate) struct Args { + /// Existing secret key file (hex or SDK JSON). + #[arg(long, value_name = "PATH")] + pub(crate) key: PathBuf, + + /// Message or encrypted payload; omit to read UTF-8 from stdin. + #[arg(value_name = "TEXT")] + pub(crate) input: Option, +} + +pub(crate) fn run_encrypt(args: Args, json: bool) -> Result<()> { + let cipher = load_cipher(&args.key)?; + let message = read_input(args.input, io::stdin().lock())?; + let ciphertext = encrypt(&cipher, &message)?; + write_output("ciphertext", &ciphertext, json, io::stdout().lock()) +} + +pub(crate) fn run_decrypt(args: Args, json: bool) -> Result<()> { + let cipher = load_cipher(&args.key)?; + let payload = read_input(args.input, io::stdin().lock())?; + let message = decrypt(&cipher, &payload)?; + write_output("message", &message, json, io::stdout().lock()) +} + +fn load_cipher(path: &Path) -> Result { + let (keypair, _) = load_existing_key(path, "secret key")?; + let mut key = [0u8; 32]; + Hkdf::::new(None, &keypair.secret_bytes()) + .expand(KEY_CONTEXT, &mut key) + .map_err(|_| CliError::MessageCrypto("key derivation failed".into()))?; + Aes256Gcm::new_from_slice(&key) + .map_err(|_| CliError::MessageCrypto("invalid AES key length".into())) +} + +fn encrypt(cipher: &Aes256Gcm, message: &str) -> Result { + let mut nonce = [0u8; NONCE_SIZE]; + OsRng + .try_fill_bytes(&mut nonce) + .map_err(|error| CliError::MessageCrypto(format!("OS randomness unavailable: {error}")))?; + encrypt_with_nonce(cipher, message, nonce) +} + +fn encrypt_with_nonce( + cipher: &Aes256Gcm, + message: &str, + nonce: [u8; NONCE_SIZE], +) -> Result { + let ciphertext = cipher + .encrypt( + &nonce.into(), + Payload { + msg: message.as_bytes(), + aad: &[VERSION], + }, + ) + .map_err(|_| CliError::MessageCrypto("encryption failed".into()))?; + let mut payload = Vec::with_capacity(1 + NONCE_SIZE + ciphertext.len()); + payload.push(VERSION); + payload.extend_from_slice(&nonce); + payload.extend_from_slice(&ciphertext); + Ok(STANDARD.encode(payload)) +} + +fn decrypt(cipher: &Aes256Gcm, encoded: &str) -> Result { + let payload = STANDARD + .decode(encoded.trim()) + .map_err(|_| CliError::MessageCrypto("invalid Base64 payload".into()))?; + if payload.len() < 1 + NONCE_SIZE + TAG_SIZE { + return Err(CliError::MessageCrypto("truncated payload".into())); + } + if payload[0] != VERSION { + return Err(CliError::MessageCrypto( + "unsupported payload version".into(), + )); + } + let mut nonce = [0u8; NONCE_SIZE]; + nonce.copy_from_slice(&payload[1..1 + NONCE_SIZE]); + let plaintext = cipher + .decrypt( + &nonce.into(), + Payload { + msg: &payload[1 + NONCE_SIZE..], + aad: &payload[..1], + }, + ) + .map_err(|_| { + CliError::MessageCrypto("authentication failed: wrong key or modified payload".into()) + })?; + String::from_utf8(plaintext) + .map_err(|_| CliError::MessageCrypto("decrypted message is not valid UTF-8".into())) +} + +fn read_input(input: Option, mut reader: impl Read) -> Result { + if let Some(input) = input { + return Ok(input); + } + let mut input = String::new(); + reader.read_to_string(&mut input).map_err(|error| { + CliError::MessageCrypto(format!("cannot read UTF-8 from stdin: {error}")) + })?; + Ok(input) +} + +fn write_output(field: &str, value: &str, json: bool, mut writer: impl Write) -> Result<()> { + let output = if json { + format!("{}\n", serde_json::json!({ (field): value })) + } else if field == "ciphertext" { + format!("{value}\n") + } else { + value.to_owned() + }; + writer + .write_all(output.as_bytes()) + .and_then(|()| writer.flush()) + .map_err(|error| CliError::MessageCrypto(format!("cannot write stdout: {error}"))) +} + +#[cfg(test)] +mod tests { + use clap::Parser; + + use crate::cli::{Cli, Command}; + + use super::*; + + // Independently generated with Python cryptography's HKDF(SHA256) and AESGCM: + // secret = bytes([1]) * 32, salt = None, info = KEY_CONTEXT, + // nonce = bytes(range(12)), AAD = b"\x01", message = b"Hello, Arch!\n". + const FIXTURE: &str = "AQABAgMEBQYHCAkKC8Zs1xInwoOttzy6nwpkRBdnxtg9Cowl6kjvbK5n"; + + fn cipher() -> Aes256Gcm { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("secret.key"); + std::fs::write(&path, "01".repeat(32)).unwrap(); + load_cipher(&path).unwrap() + } + + #[test] + fn matches_independent_fixture() { + let cipher = cipher(); + assert_eq!(decrypt(&cipher, FIXTURE).unwrap(), "Hello, Arch!\n"); + assert_eq!( + encrypt_with_nonce(&cipher, "Hello, Arch!\n", std::array::from_fn(|i| i as u8)) + .unwrap(), + FIXTURE + ); + } + + #[test] + fn round_trips_messages_with_both_key_formats() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("secret.key"); + // SDK key files can also contain public-key bytes after the secret. + let sdk_key = serde_json::to_string(&vec![1u8; 64]).unwrap(); + for contents in ["01".repeat(32), sdk_key] { + std::fs::write(&path, &contents).unwrap(); + let cipher = load_cipher(&path).unwrap(); + assert_eq!(decrypt(&cipher, FIXTURE).unwrap(), "Hello, Arch!\n"); + for message in ["", "Hello", "Hello 🌍 مرحبا", " \tfirst\nsecond\r\n "] { + let encrypted = encrypt(&cipher, message).unwrap(); + assert_eq!(decrypt(&cipher, &encrypted).unwrap(), message); + } + assert_eq!(std::fs::read_to_string(&path).unwrap(), contents); + } + } + + #[test] + fn generates_a_fresh_nonce_for_each_message() { + let cipher = cipher(); + let first = STANDARD.decode(encrypt(&cipher, "Hello").unwrap()).unwrap(); + let second = STANDARD.decode(encrypt(&cipher, "Hello").unwrap()).unwrap(); + assert_ne!(&first[1..13], &second[1..13]); + } + + #[test] + fn rejects_wrong_keys_and_modified_payloads() { + let cipher = cipher(); + let wrong_cipher = Aes256Gcm::new_from_slice(&[2; 32]).unwrap(); + let expected = decrypt(&wrong_cipher, FIXTURE).unwrap_err().to_string(); + assert!(expected.contains("authentication failed")); + let payload = STANDARD.decode(FIXTURE).unwrap(); + // Cover every byte of the nonce, ciphertext, and authentication tag. + for index in 1..payload.len() { + let mut changed = payload.clone(); + changed[index] ^= 1; + assert_eq!( + decrypt(&cipher, &STANDARD.encode(changed)) + .unwrap_err() + .to_string(), + expected + ); + } + } + + #[test] + fn rejects_malformed_truncated_and_unknown_payloads() { + let cipher = cipher(); + assert!( + decrypt(&cipher, "%%%") + .unwrap_err() + .to_string() + .contains("Base64") + ); + for length in 0..1 + NONCE_SIZE + TAG_SIZE { + assert!( + decrypt(&cipher, &STANDARD.encode(vec![VERSION; length])) + .unwrap_err() + .to_string() + .contains("truncated") + ); + } + let mut payload = STANDARD.decode(FIXTURE).unwrap(); + payload[0] = 2; + assert!( + decrypt(&cipher, &STANDARD.encode(payload)) + .unwrap_err() + .to_string() + .contains("unsupported payload version") + ); + assert_eq!( + decrypt(&cipher, &format!(" \n{FIXTURE}\r\n")).unwrap(), + "Hello, Arch!\n" + ); + } + + #[test] + fn rejects_authenticated_non_utf8_messages() { + // Same independent Python fixture parameters, plaintext = b"\xff". + let payload = "AQABAgMEBQYHCAkKC3EPxr6c4IIqkeNT8g17HPe6"; + assert!( + decrypt(&cipher(), payload) + .unwrap_err() + .to_string() + .contains("not valid UTF-8") + ); + } + + #[test] + fn rejects_missing_and_invalid_keys() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("secret.key"); + assert!(load_cipher(&path).is_err()); + assert!(!path.exists()); + std::fs::write(&path, "invalid key").unwrap(); + assert!(load_cipher(&path).is_err()); + } + + #[test] + fn accepts_commands_with_arguments_or_stdin() { + for command in ["encrypt", "decrypt"] { + for text in [None, Some(""), Some("hello")] { + let mut argv = vec!["arch-kit", command, "--key", "authority.key", "--json"]; + argv.extend(text); + let cli = Cli::try_parse_from(argv).unwrap(); + assert!(cli.json); + let args = match cli.command { + Command::Encrypt(args) if command == "encrypt" => args, + Command::Decrypt(args) if command == "decrypt" => args, + _ => panic!("unexpected command"), + }; + assert_eq!(args.key, PathBuf::from("authority.key")); + assert_eq!(args.input.as_deref(), text); + } + assert!(Cli::try_parse_from(["arch-kit", command, "hello"]).is_err()); + } + } + + #[test] + fn preserves_input_and_rejects_non_utf8_stdin() { + let message = " \nHello 🌍\r\n "; + assert_eq!(read_input(None, message.as_bytes()).unwrap(), message); + assert_eq!( + read_input(Some(String::new()), message.as_bytes()).unwrap(), + "" + ); + assert!(read_input(None, &[0xff][..]).is_err()); + } + + #[test] + fn writes_exact_plaintext_and_json_output() { + for (field, value) in [("message", " \nHello 🌍\r\n "), ("ciphertext", FIXTURE)] { + let mut output = Vec::new(); + write_output(field, value, false, &mut output).unwrap(); + let expected = if field == "ciphertext" { + format!("{value}\n") + } else { + value.to_owned() + }; + assert_eq!(output, expected.as_bytes()); + output.clear(); + write_output(field, value, true, &mut output).unwrap(); + assert_eq!( + serde_json::from_slice::(&output).unwrap(), + serde_json::json!({ (field): value }) + ); + } + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a995504..f078d2b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod ata; pub(crate) mod build_idl; pub(crate) mod create_mint; pub(crate) mod deploy; +pub(crate) mod encryption; pub(crate) mod faucet; pub(crate) mod health; pub(crate) mod init; diff --git a/src/error.rs b/src/error.rs index 318ca2c..885d264 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,6 +59,9 @@ pub(crate) enum CliError { #[error("vanity key search failed: {0}")] VanitySearch(String), + #[error("message encryption/decryption failed: {0}")] + MessageCrypto(String), + #[error("signer error: {0}")] Signer(String), diff --git a/src/main.rs b/src/main.rs index 53bee3d..a5357f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,8 @@ fn run() -> Result<()> { Command::BuildIdl(args) => commands::build_idl::run(args), Command::Keygen(args) => commands::keygen::run(args), Command::Pubkey(args) => commands::pubkey::run(args), + Command::Encrypt(args) => commands::encryption::run_encrypt(args, json), + Command::Decrypt(args) => commands::encryption::run_decrypt(args, json), Command::Ata(args) => commands::ata::run(args), Command::TokenBalance(args) => { commands::token_balance::run(&network::config(rpc_url, bitcoin_network)?, args, json)