🔐

ECDH鍵交換を用いた共通鍵暗号の検証 (Rust)

2024/10/13に公開

はじめに

ECDH (Elliptic-curve Diffie–Hellman)鍵交換という仕組みを使うことにより、自分の秘密鍵と相手の公開鍵から共通鍵が生成できると教わったので、Rustで試してみました。

ECDH鍵交換アルゴリズムと共通鍵暗号のRust実装は、すでに専門の方々によって行われCrateとして公開されていますので、こちらを使用することにします。

1. ECDH鍵交換

(1) プロジェクト作成

cargo newコマンドを実行して、プロジェクトを作成します。
ECDH鍵交換のため、secp256k1を使用します。

$ cargo new ecdhtest
$ cd ecdhtest
$ cargo add hex-literal secp256k1
Cargo.toml
[package]
name = "ecdhtest"
version = "0.1.0"
edition = "2021"

[dependencies]
hex-literal = "0.4.1"
secp256k1 = "0.30.0"

(2) プログラム作成

main.rs
use hex_literal::hex;

fn main() {
    let secp = secp256k1::Secp256k1::new();

    // STEP-1: Alice's Key
    let alice_private_key = secp256k1::SecretKey::from_slice(&hex!("de5091a9ce0842cea304bae568a20a9c8a7169db6ab23702862131074ec2e1b2")).expect("Invalid Key");
    let alice_public_key  = alice_private_key.public_key(&secp);

    // STEP-2: Bob's Key
    let bob_private_key = secp256k1::SecretKey::from_slice(&hex!("874c9170bbf894c400afab5f10db6e28fe395dd97c51c7d484d6300371197eb5")).expect("Invalid Key");
    let bob_public_key  = bob_private_key.public_key(&secp);

    // STEP-3: ECDH Shared Secret
    let alice_ecdh_secret = secp256k1::ecdh::SharedSecret::new(&bob_public_key,   &alice_private_key);
    let bob_ecdh_secret   = secp256k1::ecdh::SharedSecret::new(&alice_public_key, &bob_private_key);

    println!("Alice's PrivateKey:   {:02x?}", alice_private_key.secret_bytes());
    println!("Alice's PublicKey:    {:02x?}", alice_public_key.serialize());
    println!("Alice's SharedSecret: {:02x?}", alice_ecdh_secret.secret_bytes());
    println!("Bob's PrivateKey:     {:02x?}", bob_private_key.secret_bytes());
    println!("Bob's PublicKey:      {:02x?}", bob_public_key.serialize());
    println!("Bob's SharedSecret:   {:02x?}", bob_ecdh_secret.secret_bytes());
}

※ここに記載した秘密鍵はあくまでもテスト用です。本記事により公開されていますので、重要な資産の管理には使用しないようにしてください。

(3) プログラム実行

以下の実行結果の通り、AliceとBobで鍵の共有ができていることを分かります。

$ cargo run
︙
Alice's PrivateKey:   [de, 50, 91, a9, ce, 08, 42, ce, a3, 04, ba, e5, 68, a2, 0a, 9c, 8a, 71, 69, db, 6a, b2, 37, 02, 86, 21, 31, 07, 4e, c2, e1, b2]
Alice's PublicKey:    [03, ef, e2, 88, e9, 2a, d8, b7, 27, 95, 7e, 4c, db, 48, b5, 4b, 24, 7d, a9, 67, d7, 9f, 26, e1, 43, a8, 57, 34, e9, 22, b5, 54, 4a]
Alice's SharedSecret: [34, 2b, ff, e5, b3, 98, 63, d5, fb, 93, 1a, 32, e3, aa, 34, d3, cd, 2b, d6, dd, 14, 68, df, 25, e1, 0a, 35, 61, 59, 6b, 7b, 3b]
Bob's PrivateKey:     [87, 4c, 91, 70, bb, f8, 94, c4, 00, af, ab, 5f, 10, db, 6e, 28, fe, 39, 5d, d9, 7c, 51, c7, d4, 84, d6, 30, 03, 71, 19, 7e, b5]
Bob's PublicKey:      [03, ea, 98, 69, a9, 06, e3, 60, 6b, 9f, 66, 2f, cf, 41, a5, 41, fb, ef, 60, f1, 42, 97, 99, 02, 46, 4d, eb, be, 18, c8, e4, f9, e0]
Bob's SharedSecret:   [34, 2b, ff, e5, b3, 98, 63, d5, fb, 93, 1a, 32, e3, aa, 34, d3, cd, 2b, d6, dd, 14, 68, df, 25, e1, 0a, 35, 61, 59, 6b, 7b, 3b]

2. 共通鍵暗号

共通鍵が得られましたので、暗号化して復号できることを試してみましょう。
ここでは、分かりやすくするため別のプロジェクトとして用意することにしましょう。

上記のECDH鍵交換の実行結果では、以下の共通鍵 (256bit)が得られましたのでこれを使うことにします。
実際の運用においては、ECDH鍵交換で得られた値をそのまま暗号鍵として使うのではなく、sha256でハッシュ化等する方がセキュリティ的には安全かと思います。

ECDH鍵交換で取得した共通鍵
342bffe5b39863d5fb931a32e3aa34d3cd2bd6dd1468df25e10a3561596b7b3b

(1) プロジェクト作成

cargo newコマンドを実行して、プロジェクトを作成します。
共通鍵暗号アリゴリズムとしてAES256 CBCを利用するため、aesblock-paddingcbcを使用します。
また、AESのinitialization vector (IV)パラメータの生成にrandを用いています。

$ cargo new aestest
$ cd aestest
$ cargo add aes block-padding hex-literal rand
$ cargo add cbc --features=std
Cargo.toml
[package]
name = "aestest"
version = "0.1.0"
edition = "2021"

[dependencies]
aes = "0.8.4"
block-padding = "0.3.3"
cbc = { version = "0.1.2", features = ["std"] }
hex-literal = "0.4.1"
rand = "0.8.5"

(2) プログラム作成

encrypt()関数とdecrypt()関数を宣言し、ECDH鍵交換で生成した共通鍵を引数として渡しています。
暗号データの先頭16バイトにIVを付与し、復号時にIVを設定しています。

main.rs
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, BlockDecryptMut, KeyIvInit};
use hex_literal::hex;
use rand::Rng;
use std::vec::Vec;

type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;

fn encrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
    let iv = rand::thread_rng().gen::<[u8; 16]>();
    let cipher = Aes256CbcEnc::new(key.into(), &iv.into()).encrypt_padded_vec_mut::<Pkcs7>(&data);

    let mut encrypted = Vec::from(iv);
    encrypted.extend(cipher);
    encrypted
}

fn decrypt(key: &[u8;32], data: &[u8]) -> Vec<u8> {
    let decrypted = Aes256CbcDec::new(key.into(), data[0..16].into())
        .decrypt_padded_vec_mut::<Pkcs7>(&data[16..])
        .unwrap();
    decrypted
}

fn main() {
    let key = hex!("342bffe5b39863d5fb931a32e3aa34d3cd2bd6dd1468df25e10a3561596b7b3b");
    let data = "テスト".as_bytes();

    let encrypted = encrypt(&key, &data);
    let decrypted = decrypt(&key, &encrypted);

    println!("plain:       {:02x?}", data);
    println!("key:         {:02x?}", key);
    println!("encrypted:   {:02x?}", encrypted);
    println!("decrypted:   {:02x?}", decrypted);
}

(3) プログラム実行

$ cargo run
︙
plain:       [e3, 83, 86, e3, 82, b9, e3, 83, 88]
key:         [34, 2b, ff, e5, b3, 98, 63, d5, fb, 93, 1a, 32, e3, aa, 34, d3, cd, 2b, d6, dd, 14, 68, df, 25, e1, 0a, 35, 61, 59, 6b, 7b, 3b]
encrypted:   [31, 00, 99, 00, 6c, c8, dc, 9a, ef, ce, f5, 79, 03, c0, f2, 58, 35, 5f, e5, 76, da, d3, dc, 35, 5c, 03, 24, 82, 57, 74, ca, 61]
decrypted:   [e3, 83, 86, e3, 82, b9, e3, 83, 88]

IC Canister上で動作させるには

本記事で紹介した仕組みを、分散型クラウドであるInternet Computer (IC)のCanisterで利用しようとすると、ivの生成に使用しているrandが問題となります。
一般的にblockchainではその性質上、標準的な乱数機能は利用できませんので、例えば以下のようにrandの代わりにrand-icなどの代替ライブラリを使用することで回避できます。

$ cargo add rand-ic
lib.rs
use rand_ic::Rand;

let mut iv = [0u8; 16];
Rand::new().fill_u8(&mut iv);

End-to-End暗号化の方法の一つとして利用できるでしょう。

注意事項

本記事は個人的な学習のために検証したものにすぎませんので、実際のご使用にあたっては各自の責任の下でお願いします。

Discussion