🎃

Rustで共有鍵で暗号化・復号化をする

2024/12/12に公開

目的

ユニークビジョン株式会社 Advent Calendar 2024のシリーズ2、12/4の記事です。

複数のプログラムで共有鍵を使った暗号化されたデータをやり取りすることがあります。
以前はAESを使ってましたが、AESよりもシンプルで計算が早いChaCha20Poly1305を使ってみます。
特にnonceが192bitのXChaCha20Poly1305を使います。ちなみに通常のnonceは96bitです。
こちらのcrateを利用します。
chacha20poly1305

説明

nonce

nonceは使い捨ての値で、被らなけれななんでも良いです。crateで紹介されているサンプルではランダムな値が生成されています。

let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);

ここではUUIDv7を採用します。UUIDは128bitなので残りの64bitはランダムな値で補填します。

pub fn make_nonce() -> Vec<u8> {
    let uuid = Uuid::now_v7();
    let mut res = uuid.as_bytes().to_vec();
    let mut rng = rand::rngs::StdRng::from_entropy();
    res.extend(rng.gen::<[u8; 8]>().to_vec());
    res
}

暗号化

暗号化した結果はnonce + 暗号化したデータというバイト列にしてbase64で文字列化します。

復号化

暗号化したデータから元の文字列とUUIDを返します。UUIDv7なので暗号化された時間がわかります。

コード

main.rs
use base64::prelude::*;
use chacha20poly1305::aead::Aead;
use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce};
use chrono::prelude::*;
use rand::{Rng, SeedableRng};
use uuid::Uuid;

#[derive(Debug, thiserror::Error)]
pub enum EncryptError {
    #[error("Invalid {0}")]
    Invalid(String),

    #[error("Base64 {0}")]
    Base64(#[from] base64::DecodeError),

    #[error("Utf8 {0}")]
    Utf8(#[from] std::string::FromUtf8Error),

    #[error("Chacha20 {0}")]
    Chacha20(#[from] chacha20poly1305::Error),
}

// 暗号化とBase64エンコード
pub fn encrypt_with_base64(key: &str, data: &str) -> Result<String, EncryptError> {
    let res = encrypt(key.as_bytes(), data.as_bytes())?;
    Ok(BASE64_STANDARD.encode(res))
}

// 復号化とBase64デコード。NonceがUUIDv7なので、UUIDも返す
pub fn decrypt_with_base64(key: &str, data: &str) -> Result<(String, Uuid), EncryptError> {
    let data = BASE64_STANDARD.decode(data)?;
    let uuid = get_uuid(&data)?;
    let plaintext = decrypt(key.as_bytes(), &data)?;
    String::from_utf8(plaintext.to_vec())
        .map_err(|e| e.into())
        .map(|s| (s, uuid))
}

// UUID取得
pub fn get_uuid(data: &[u8]) -> Result<Uuid, EncryptError> {
    let uuid_src = data[0..16]
        .to_vec()
        .try_into()
        .map_err(|_| EncryptError::Invalid("size invalid".to_string()))?;
    Ok(Uuid::from_bytes(uuid_src))
}

// Nonce生成。先頭16byteはUUID、残り8byteは乱数
pub fn make_nonce() -> Vec<u8> {
    let uuid = Uuid::now_v7();
    let mut res = uuid.as_bytes().to_vec();
    let mut rng = rand::rngs::StdRng::from_entropy();
    res.extend(rng.gen::<[u8; 8]>().to_vec());
    res
}

// 暗号化
pub fn encrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>, EncryptError> {
    let cipher = XChaCha20Poly1305::new(key.into());
    //let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); // 192-bits; unique per message
    let nonce_src = make_nonce();
    let nonce = XNonce::from_slice(&nonce_src);
    let ciphertext = cipher.encrypt(nonce, data)?;
    let mut res = nonce.to_vec();
    res.extend(ciphertext);
    Ok(res)
}

// 復号化
pub fn decrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>, EncryptError> {
    let nonce = &data[0..24];
    let cipher = XChaCha20Poly1305::new(key.into());
    let plaintext = cipher.decrypt(nonce.into(), &data[24..])?;
    Ok(plaintext.to_vec())
}

// UUIDからUTCを取得
pub fn get_utc(uuid: &Uuid) -> Option<DateTime<Utc>> {
    if let Some(timestamp) = uuid.get_timestamp() {
        let (secs, nsecs) = timestamp.to_unix();
        Utc.timestamp_opt(secs as i64, nsecs).single()
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // RUST_LOG=info REALM_CODE=test cargo test -p common test_common_encript_chacha20 -- --nocapture --test-threads=1
    #[tokio::test]
    async fn test_common_encript_chacha20() -> anyhow::Result<()> {
        let plaintext = "予定表~①💖ハンカクだ";
        let key = "01234567890123456789012345678901"; // 32byte
        let enc = encrypt_with_base64(key, plaintext)?;
        let dec = decrypt_with_base64(key, &enc)?;
        assert_eq!(plaintext, dec.0);
        println!("{}\n{}\n{:?}", enc, dec.0, get_utc(&dec.1));
        Ok(())
    }
}
実行結果
AZO4Hj4icBKkdrtgu2Iv2yBmL4wOcQtHfI9ruCEqDGa/czACi4hItIV6CotJRqg4L48HlLEsilnCY0v2F2MV5RoDhyflxUgbyK8=
予定表~①💖ハンカクだ
Some(2024-12-11T23:46:50.018Z)

Discussion