PR

Rust で ChaCha20-Poly1305 を使い暗号化と復号を実装してみた

1. はじめに

ChaCha20-Poly1305 は、暗号化と認証を同時に行う AEAD (Authenticated Encryption with Associated Data) と呼ばれる暗号方式です。
昔いろいろとあって IETF(Internet Engineering Task Force) で ChaCha20-Poly1305 を標準化が進められたようです。
パッケージも用意されており、Rust で簡単に使うことができます。

2. パッケージの追加

Cargo.toml に必要となるパッケージを追加します。

[dependencies]
chacha20poly1305 = "0.10.1"
rand = "0.8.5"
base64 = "0.22.1"

3. 以下、ソース

use base64::{engine::general_purpose, Engine as _};
use chacha20poly1305::aead::{Aead, KeyInit, OsRng};
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
use chacha20poly1305::aead::generic_array::typenum::U32;
use chacha20poly1305::aead::generic_array::GenericArray;
use rand::RngCore;
use std::str;

// ChaCha20Poly1305 で暗号化されたデータを復号する
fn decrypt(base_key: &[u8], nonce: &[u8], ciphertext: &[u8]) -> Vec<u8> {
    let key = Key::from_slice(base_key);
    let cipher = ChaCha20Poly1305::new(key);
    let nonce = Nonce::from_slice(nonce);

    cipher
        .decrypt(nonce, ciphertext)
        .expect("decryption failure!")
}

// ChaCha20Poly1305 でデータを暗号化する
fn encrypt(base_key: &[u8], plaintext: &[u8]) -> (Vec<u8>, Vec<u8>) {
    let key = Key::from_slice(base_key);
    let cipher = ChaCha20Poly1305::new(key);

    // 12バイトのNonceを生成
    let mut nonce = [0u8; 12];
    OsRng.fill_bytes(&mut nonce);
    let nonce = Nonce::from_slice(&nonce);

    let ciphertext = cipher
        .encrypt(nonce, plaintext)
        .expect("encryption failure!");
    (nonce.to_vec(), ciphertext)
}

// 文字列を GenericArray に変換する
fn string_to_generic_array(s: &str) -> GenericArray<u8, U32> {
    let bytes = general_purpose::STANDARD
        .decode(s)
        .expect("Decoding failed");
    GenericArray::clone_from_slice(&bytes)
}

fn main() {
    // 暗号化キーを生成
    let base_key = ChaCha20Poly1305::generate_key(&mut OsRng);
    println!("Base key: {:?}", base_key);

    let base_key_str = general_purpose::STANDARD.encode(base_key);
    println!("Base key: {:?}", base_key_str);
    let base_key = string_to_generic_array(&base_key_str);
    println!("Base key: {:?}", base_key);

    let plaintext = "Hello, world!".as_bytes();
    let (nonce, ciphertext) = encrypt(base_key.as_slice(), plaintext);
    println!("Nonce: {:?}", nonce);
    println!("Ciphertext: {:?}", ciphertext);

    let nonce_str = general_purpose::STANDARD.encode(&nonce);
    let ciphertext_str = general_purpose::STANDARD.encode(&ciphertext);

    println!("{}", nonce_str);
    println!("{}", ciphertext_str);

    let decrypted_plaintext = decrypt(base_key.as_slice(), &nonce, &ciphertext);
    println!(
        "Decrypted: {:?}",
        String::from_utf8(decrypted_plaintext).unwrap()
    );
}

キーとNonceを使ってワンタイムキーを生成し暗号化を行います。
暗号化されたデータは、復号するために必要なNonceと一緒に扱う必要があります。
キーに関してはアプリ間で共通。Nonceはメッセージごとに異なる値を使うことが必要です。

4. 復号のみ

fn main() {
    let base_key = general_purpose::STANDARD
        .decode("base_key_str で出力された文字列")
        .expect("Failed to decode base key");
    let nonce = general_purpose::STANDARD
        .decode("nonce_str で出力された文字列")
        .expect("Failed to decode nonce");
    let ciphertext = general_purpose::STANDARD
        .decode("ciphertext_str で出力された文字列")
        .expect("Failed to decode ciphertext");

    println!("Base key: {:?}", base_key);
    println!("Nonce: {:?}", nonce);
    println!("Ciphertext: {:?}", ciphertext);

    let decrypted_plaintext = decrypt(&base_key, &nonce, &ciphertext);
    println!(
        "Decrypted: {:?}",
        String::from_utf8(decrypted_plaintext).unwrap()
    );
}

.env にパスワードなど直書きするのではなく、事前に暗号化しておいて、復号して使うという使い方もできます。

5. まとめ

細かい仕様については、参考サイトを参照してください。
正直解説を読んでいても、よくわからない部分が多かったです。
平文のサイズが 512 bits 以上の場合、Counter がインクリメントして・・・とか、実際どうなるの?といった感じです。おそらくいい感じにしてくれると思いたい。
最後に書いた通り、.env でパスワードを暗号化するためだけに用意したコードなので実用性はあまりないかもしれません。
似たようなコードを書く際の一助になればと思います。

今回の記事も、Github Copilot が7割くらい書いてくれました。

A. 参考サイト

ChaCha20-Poly1305の解説と実装
ChaCha20をRustで実装してみた
新しいTLSの暗号方式ChaCha20-Poly1305

B. 参考書籍

コメント