Skip to main content

outlayer_cli/
crypto.rs

1use anyhow::{Context, Result};
2use chacha20poly1305::aead::Aead;
3use chacha20poly1305::{AeadCore, ChaCha20Poly1305, KeyInit};
4use rand::rngs::OsRng;
5
6/// Encrypt plaintext with ChaCha20-Poly1305 using hex-encoded pubkey as symmetric key.
7///
8/// Matches dashboard's JS implementation exactly:
9/// 1. Parse hex pubkey → 32-byte key
10/// 2. Generate 12-byte random nonce
11/// 3. Encrypt plaintext
12/// 4. Result: base64([12-byte nonce] + [ciphertext + 16-byte auth tag])
13pub fn encrypt_secrets(pubkey_hex: &str, plaintext: &str) -> Result<String> {
14    let key_bytes = hex::decode(pubkey_hex).context("Invalid hex pubkey")?;
15    anyhow::ensure!(key_bytes.len() == 32, "Pubkey must be 32 bytes");
16
17    let key = chacha20poly1305::Key::from_slice(&key_bytes);
18    let cipher = ChaCha20Poly1305::new(key);
19    let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
20
21    let ciphertext = cipher
22        .encrypt(&nonce, plaintext.as_bytes())
23        .map_err(|e| anyhow::anyhow!("Encryption failed: {e}"))?;
24
25    // [12-byte nonce] + [ciphertext + 16-byte tag]
26    let mut encrypted = Vec::with_capacity(12 + ciphertext.len());
27    encrypted.extend_from_slice(&nonce);
28    encrypted.extend_from_slice(&ciphertext);
29
30    Ok(base64::Engine::encode(
31        &base64::engine::general_purpose::STANDARD,
32        &encrypted,
33    ))
34}
35
36/// Generate a 32-byte random hex string for payment key secret
37pub fn generate_payment_key_secret() -> String {
38    let mut bytes = [0u8; 32];
39    rand::RngCore::fill_bytes(&mut OsRng, &mut bytes);
40    hex::encode(bytes)
41}
42
43/// Sign a NEP-413 message using a NEAR private key (for `secrets update`).
44///
45/// Returns `(signature_str, public_key_str, nonce_base64)`.
46/// - `signature_str`: near-crypto Signature string (e.g. "ed25519:base58...")
47/// - `public_key_str`: near-crypto PublicKey string (e.g. "ed25519:base58...")
48/// - `nonce_base64`: base64-encoded random 32-byte nonce
49pub fn sign_nep413(
50    private_key: &str,
51    message: &str,
52    recipient: &str,
53) -> Result<(String, String, String)> {
54    use borsh::BorshSerialize;
55    use near_crypto::SecretKey;
56    use sha2::{Digest, Sha256};
57
58    let secret_key: SecretKey = private_key
59        .parse()
60        .context("Invalid private key for NEP-413 signing")?;
61    let public_key = secret_key.public_key();
62
63    // Random 32-byte nonce
64    let mut nonce = [0u8; 32];
65    rand::RngCore::fill_bytes(&mut OsRng, &mut nonce);
66
67    // NEP-413 payload (Borsh-serialized)
68    #[derive(BorshSerialize)]
69    struct Nep413Payload {
70        message: String,
71        nonce: [u8; 32],
72        recipient: String,
73        callback_url: Option<String>,
74    }
75
76    let payload = Nep413Payload {
77        message: message.to_string(),
78        nonce,
79        recipient: recipient.to_string(),
80        callback_url: None,
81    };
82
83    // NEP-413 tag: 2**31 + 413 = 2147484061
84    let tag: u32 = 2_147_484_061;
85    let mut data = borsh::to_vec(&tag)?;
86    let payload_bytes = borsh::to_vec(&payload)?;
87    data.extend_from_slice(&payload_bytes);
88
89    // SHA-256 hash, then sign
90    let hash = Sha256::digest(&data);
91    let signature = secret_key.sign(&hash);
92
93    let nonce_base64 = base64::Engine::encode(
94        &base64::engine::general_purpose::STANDARD,
95        &nonce,
96    );
97
98    Ok((signature.to_string(), public_key.to_string(), nonce_base64))
99}