ChaCha20をRustで実装してみた
はじめに
暗号シリーズ
今回は疑似乱数で一番有名なChaCha20を解説していく
以下のような人向け
- ChaCha20ってなに
- ChaCha20は知っているけどアルゴリズムは知らない
- 疑似乱数の基本を知りたい
Rustで実装していく。
ソースコードはここ
ChaCha20とは
djb作共通鍵暗号アルゴリズムSalsa20の後継。
普通に共通鍵暗号として使用されることはほとんどなく
(..多分ないけど有ったら教えてほしい)、
実用上では疑似乱数アルゴリズムとして使用されている。
追記:SSHやTLS、IPSecでちゃんと使用されている とご教授頂いた。
AES-GCMに勝ってるのすげ~。
ちゃんと現役のアルゴリズムなので訂正。
ハードウェアのサポートなしのソフトウェアアルゴリズムのみで言えば、AESよりも高速らしい共通鍵暗号アルゴリズム。
(AESが気になる人はここで解説してるので見て)
アルゴリズム部分では共通鍵暗号として解説していき、その後、疑似乱数アルゴリズムとしての利用方法の説明を行う。
疑似乱数アルゴリズムとしての利用では、Linuxで使用されている。
(Linux疑似乱数アルゴリズムの調査は別記事で作成する)
疑似乱数
疑似乱数とは、ざっくり言えば
入力が同じであれば、出力は同じになる乱数らしきもの
である。
それだけ聞くと、乱数とは程遠いものに思えるが現代ではそれを乱数として利用できている。
なぜなら実運用上では、真正乱数と言われる計算に時間がかかるが完全に予測できない乱数を作成し、その値を基にして疑似乱数を作成するから。
つまり、
- 真正乱数(=完全に推測不可能な乱数)作成 ←超重要処理!!
- 疑似乱数生成器を作成
- それを乱数生成器に入力する
- 値を出力
という流れになる。
現状速度面を考慮すると、大体のPCで同じ手順を行っている。
なので実質、乱数=疑似乱数と考えて良い。
勘のいい人は理解していると思うが、真正乱数が判明したら全ての暗号、ハッシュ等が無意味になったと思っていい。
それだけ真正乱数部分は重要になる。
最後に少しだけ同じ乱数を発生させる実験を行うので、興味がでたら読んでほしい。
アルゴリズム
以下ではChaChaで使用される暗号化アルゴリズムについて解説していく。RFC7539を読んで解説する。
パラメータ
暗号系あるあるだが、事前にいくつかパラメータが設定されている。
変数 | データサイズ | 説明 |
---|---|---|
Word | 4Byte | 最小単位 |
Block | 64Byte | 暗号化アルゴリズムの入力最大単位。データが大きい場合、ブロック数が増える |
Key | 32Byte | 秘密鍵 データ一つにつき一つ |
nonce | 12Byte | 適当な定数 同上 |
counter | 1Byte | ブロック用カウンター |
const | 12Byte | ブロック初期化用定数 |
ざっくりとした説明だが、これで読み進められると思う。
定数
初期化設定の際、
constは以下の値を使用する。
block[0] = 0x61707865;
block[1] = 0x3320646e;
block[2] = 0x79622d32;
block[3] = 0x6b206574;
これは"expand 32byte-k"のASCIIコードを4分割したもの。
暗号系では設計者がバックドアを仕掛けることが容易なので、それに対して種も仕掛けもない定数を決めておく必要がある。
ただ、定数を使用するというのはバックドアの危険性が消えるわけではないので、
論文が言っているから信じよう、
この人が言っているから信じよう、
とかではなく仕組みを理解して信じる必要がある。
できれば、
- 無意味な文字列
- 素数のルート
- 無理数の何桁目
等。
今回で言えば、"expand 32byte-k"を見て完全に無意味な文字列であるからバックドアではなく安心できる、みたいな話。
特にセキュリティ関係の人は、気にしすぎて困ることはないから覚えておいてほしい。
ちなみに、上記ですらバックドアの可能性があるらしい
...もうどうしろと?
まぁここまで言っておいてなんだけど、大体は大丈夫だとは思う。
なぜなら、自分より暗号学で優れた人たちが審査しているからね!
...人で判断してたわ。
うん 次に進もう。
QUARTERROUND
ChaChaの最も重要な部分。
他の暗号系で言えば混合関数にあたる。
4wordの入力をぐちゃぐちゃに混ぜる関数。
fn quarter_round(state: &mut [u32], i: usize, j: usize, k: usize, l: usize) {
/*
1. a += b; d ^= a; d <<<= 16;
2. c += d; b ^= c; b <<<= 12;
3. a += b; d ^= a; d <<<= 8;
4. c += d; b ^= c; b <<<= 7;
*/
//let rot32 = |x: u32, n: u32| x << n | x >> (32 - n);
let mut a = state[i];
let mut b = state[j];
let mut c = state[k];
let mut d = state[l];
a = a.wrapping_add(b);
d ^= a;
d = d.rotate_left(16);
c = c.wrapping_add(d);
b ^= c;
b = b.rotate_left(12);
a = a.wrapping_add(b);
d ^= a;
d = d.rotate_left(8);
c = c.wrapping_add(d);
b ^= c;
b = b.rotate_left(7);寄与
state[i] = a;
state[j] = b;
state[k] = c;
state[l] = d;
}
基礎であり、奥義みたいな存在。
処理内容自体を観察してみると、
- a,cをb,dを足して上書き変更
- それを使ってd,bをマスク
- d,bをシフト
となっている。
あえて処理工程を偏らせることで、より攪拌する感じだと思う。
以外にも処理の簡単さに比べてちゃんと混ざる。
init
初期化処理
細かなことは色んなサイトや論文内で書かれているから省略。
ざっくり言えば、
それぞれ、
const = 先に説明したやつ。ブロック毎で同じ
key = 真正乱数。ブロック毎で同じ。
counter = ブロックのインデックス。変更可能。デフォルトは0から順にインクリメントする
nonce = なんか適当な数。ブロック毎で同じ。
実装したものがこれ
fn init(key: &[u32], counter: u32, nonce: &[u32]) -> Block {
/*
The ChaCha20 state is initialized as follows:
o The first four words (0-3) are constants: 0x61707865, 0x3320646e,
0x79622d32, 0x6b206574.
o The next eight words (4-11) are taken from the 256-bit key by
reading the bytes in little-endian order, in 4-byte chunks.
o Word 12 is a block counter. Since each block is 64-byte, a 32-bit
word is enough for 256 gigabytes of data.
o Words 13-15 are a nonce, which should not be repeated for the same
key. The 13th word is the first 32 bits of the input nonce taken
as a little-endian integer, while the 15th word is the last 32
bits.
cccccccc cccccccc cccccccc cccccccc
kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk
kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk
bbbbbbbb nnnnnnnn nnnnnnnn nnnnnnnn
c=constant k=key b=blockcount n=nonce */
/*const 0x61707865, 0x3320646e,
0x79622d32, 0x6b206574.
*/
if !(key.len() == 8 && nonce.len() == 3) {
panic!("block key size or nonce is not varid");
}
let mut block: Block = [0; 16];
block[0] = 0x61707865;
block[1] = 0x3320646e;
block[2] = 0x79622d32;
block[3] = 0x6b206574;
(0..key.len()).into_iter().for_each(|i| {
block[4 + i] = key[i];
});
block[12] = counter;
(0..nonce.len()).into_iter().for_each(|i| {
block[13 + i] = nonce[i];
});
println!("block init is {:x?}", block);
block
}
...気が付いた人いる?
実は、ブロック毎で違いはcounterのみ。
つまり初期化処理のみではブロック毎の値はほぼ同じになってしまうんだな~これ
論文から抜粋
First block setup:
61707865 3320646e 79622d32 6b206574
03020100 07060504 0b0a0908 0f0e0d0c
13121110 17161514 1b1a1918 1f1e1d1c
00000001 00000000 4a000000 00000000
Second block setup:
61707865 3320646e 79622d32 6b206574
03020100 07060504 0b0a0908 0f0e0d0c
13121110 17161514 1b1a1918 1f1e1d1c
00000002 00000000 4a000000 00000000
いやいやいや 同じすぎでしょ!
見ての通りカウンターの部分のみ違うだけ、残りはすべて同じ。
なんならカウンターの値ですら、2bitしか違いがない。
めちゃくちゃ不安になったでしょう。
安心してほしい、ちゃんとめちゃくちゃになるから。
実装していてめちゃくちゃ不安になって手が止まったのは内緒
BLOCK FUNCTION
初期化処理を行ったブロックに対して、
QUARTERROUND関数8セットを10回回す。
4セット部分は縦列で、後半4セットは斜めを対象に攪拌を行う。
最後に初期値とのXORを取る。
fn block(key: &[u32], counter: u32, nonce: &[u32]) -> Block {
let init = ChaCha::init(key, counter, nonce);
let mut state = init;
let inner_block = |state: &mut Block| {
/*
inner_block (state):
Qround(state, 0, 4, 8,12)
Qround(state, 1, 5, 9,13)
Qround(state, 2, 6,10,14)
Qround(state, 3, 7,11,15)
Qround(state, 0, 5,10,15)
Qround(state, 1, 6,11,12)
Qround(state, 2, 7, 8,13)
Qround(state, 3, 4, 9,14)
end
*/
//
ChaCha::quarter_round(state, 0, 4, 8, 12);
ChaCha::quarter_round(state, 1, 5, 9, 13);
ChaCha::quarter_round(state, 2, 6, 10, 14);
ChaCha::quarter_round(state, 3, 7, 11, 15);
ChaCha::quarter_round(state, 0, 5, 10, 15);
ChaCha::quarter_round(state, 1, 6, 11, 12);
ChaCha::quarter_round(state, 2, 7, 8, 13);
ChaCha::quarter_round(state, 3, 4, 9, 14);
};
for i in 0..10 {
inner_block(&mut state);
}
state
.iter_mut()
.enumerate()
.for_each(|(i, block)| *block = (*block).wrapping_add(init[i]));
state
}
1ブロック単位で言えばこれで暗号化は完了。
不安に思っていた初期ブロックはこれを通すとこうなる。
First block after block operation:
f3514f22 e1d91b40 6f27de2f ed1d63b8
821f138c e2062c3d ecca4f7e 78cff39e
a30a3b8a 920a6072 cd7479b5 34932bed
40ba4c79 cd343ec6 4c2c21ea b7417df0
Second block after block operation:
9f74a669 410f633f 28feca22 7ec44dec
6d34d426 738cb970 3ac5e9f3 45590cc4
da6e8b39 892c831a cdea67c1 2b7e1d90
037463f3 a11a2073 e8bcfb88 edc49139
...たった2bitの違いだけでほとんど別物になっている
凄いな~
混ぜ方と回転方向でちゃんとシャッフルされるんだから。
SERIALIZE
暗号バイトをリトルエンディアンに変換する。
pub fn serialize(state: &[u32]) -> Vec<u8> {
state
.iter()
.flat_map(|x| x.to_le_bytes())
.collect::<Vec<u8>>()
}
論文で書かれていたから作成したけど、内部で変換しているから実装では使っていない。
rustではそのままto_le_bytesを使えば良し!
ChaCha20_Encrypt
上記の関数等を使用してバイト列を暗号にしていく。
少しだけ処理が加わっているが処理内容に変わりはない。
一言で言えばバーナム暗号[1]を入力バイト列分行っていく感じ。
やっぱりバーナム暗号は最強!
論文から抜粋
chacha20_encrypt(key, counter, nonce, plaintext):
for j = 0 upto floor(len(plaintext)/64)-1
key_stream = chacha20_block(key, counter+j, nonce)
block = plaintext[(j*64)..(j*64+63)]
encrypted_message += block ^ key_stream
end
if ((len(plaintext) % 64) != 0)
j = floor(len(plaintext)/64)
key_stream = chacha20_block(key, counter+j, nonce)
block = plaintext[(j*64)..len(plaintext)-1]
encrypted_message += (block^key_stream)[0..len(plaintext)%64]
end
return encrypted_message
end
処理の流れを簡単に説明すると、
- 入力バイト列を64バイト毎に秘密鍵を作成
- 秘密鍵と入力バイト列をXOR
- リトルエンディアンで出力
となる。
fn encode(key: &[u32], counter: u32, nonce: &[u32], plain_text: &[u8]) -> Vec<u8> {
let mut encrypt_message = plain_text
.chunks(64)
.enumerate()
.flat_map(|(i, chunk)| {
println!("encrypt chunk size is {}", chunk.len());
let key_stream = ChaCha::block(key, counter + i as u32, nonce)
.iter()
.flat_map(|x| x.to_le_bytes())
.collect::<Vec<u8>>();
let mut chunk = chunk.clone().to_vec();
println!("key stream is {:x?}", key_stream);
println!("chunk {} is {:x?}", i, chunk);
for (a, b) in chunk.iter_mut().zip(key_stream.iter()) {
*a ^= b;
}
println!("{} block result is {:x?}", i, chunk);
chunk
})
.collect::<Vec<u8>>();
encrypt_message
}
実装に関しては、
最後の暗号ブロックで64バイトに足りなくなる可能性があるけど、Rustのchunksを利用すれば最大バイト分持ってきてくれるので便利!
(134バイトなら134 = 64 + 64 + 6 となりブロック3個で、最後のブロックは6バイトになる)
あとは、zip関数が入力バイトと鍵バイトを揃えてくれるから簡単!
テスト
RFCに記載されているテストベクターを参考にテストする。
QUARTERROUNDのテスト、
1ブロックのテスト、
ストリーム暗号のテスト
を記載している。
テストベクターが用意されているのありがてぇ!
感謝!!
test
#[cfg(test)]
mod test {
use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng, ChaChaRng};
use super::*;
#[test]
fn quarter_test() {
/*
o a = 0x11111111
o b = 0x01020304
o c = 0x9b8d6f43
o d = 0x01234567
o c = c + d = 0x77777777 + 0x01234567 = 0x789abcde
o b = b ^ c = 0x01020304 ^ 0x789abcde = 0x7998bfda
o b = b <<< 7 = 0x7998bfda <<< 7 = 0xcc5fed3c
o a = 0xea2a92f4
o b = 0xcb1cf8ce
o c = 0x4581472e
o d = 0x5881c4bb
*/
let mut state: [u32; 4] = [0x11111111, 0x01020304, 0x9b8d6f43, 0x01234567];
let ans: [u32; 4] = [0xea2a92f4, 0xcb1cf8ce, 0x4581472e, 0x5881c4bb];
ChaCha::quarter_round(&mut state, 0, 1, 2, 3);
assert_eq!(state, ans);
}
#[test]
fn block_test() {
/*
For a test vector, we will use the following inputs to the ChaCha20
block function:
o Key = 00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:10:11:12:13:
14:15:16:17:18:19:1a:1b:1c:1d:1e:1f. The key is a sequence of
octets with no particular structure before we copy it into the
ChaCha state.
o Nonce = (00:00:00:09:00:00:00:4a:00:00:00:00)
o Block Count = 1.
After setting up the ChaCha state, it looks like this:
ChaCha state with the key setup.
61707865 3320646e 79622d32 6b206574
03020100 07060504 0b0a0908 0f0e0d0c
13121110 17161514 1b1a1918 1f1e1d1c
00000001 09000000 4a000000 00000000
After running 20 rounds (10 column rounds interleaved with 10
"diagonal rounds"), the ChaCha state looks like this:
ChaCha state after 20 rounds
837778ab e238d763 a67ae21e 5950bb2f
c4f2d0c7 fc62bb2f 8fa018fc 3f5ec7b7
335271c2 f29489f3 eabda8fc 82e46ebd
d19c12b4 b04e16de 9e83d0cb 4e3c50a2
Finally, we add the original state to the result (simple vector or
matrix addition), giving this:
ChaCha state at the end of the ChaCha20 operation
e4e7f110 15593bd1 1fdd0f50 c47120a3
c7f4d1c7 0368c033 9aaa2204 4e6cd4c3
466482d2 09aa9f07 05d7c214 a2028bd9
d19c12b5 b94e16de e883d0cb 4e3c50a2
*/
let le_to_u32 = |x: &[u8]| {
let mut ret = 0u32;
//println!("le is {:x?} ", x);
for x in x.iter().enumerate() {
ret ^= (*x.1 as u32) << (8u32 * x.0 as u32);
}
ret
};
let be_to_u32 = |x: &[u8]| {
let mut ret = 0u32;
for x in x.iter().enumerate() {
ret += (*x.1 as u32) << (8u32 * (3 - x.0 as u32));
}
ret
};
let key = hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
.expect("key is invalid")
.chunks(4)
.map(le_to_u32)
.collect::<Vec<u32>>();
let nonce = hex::decode(b"000000090000004a00000000")
.expect("nonce is invalid")
.chunks(4)
.map(le_to_u32)
.collect::<Vec<u32>>();
let counter = 1u32;
let ans: Vec<u32> = "e4e7f110 15593bd1 1fdd0f50 c47120a3
c7f4d1c7 0368c033 9aaa2204 4e6cd4c3
466482d2 09aa9f07 05d7c214 a2028bd9
d19c12b5 b94e16de e883d0cb 4e3c50a2"
.split_whitespace()
.map(|x| {
let x = hex::decode(x).expect("ans decode fail");
let ret = u32::from_be_bytes(x.try_into().unwrap());
ret
})
.collect();
//println!("ans is {:x?}", ans);
println!(
"key is {:x?} , nonce is {:x?} , counter is {}",
key, nonce, counter
);
let chacha = ChaCha::block(&key, counter, &nonce);
println!("block vec is {:x?}", chacha);
assert_eq!(ans, chacha);
println!("serialize block is {:x?}", ChaCha::serialize(&chacha));
}
#[test]
fn encode_test() {
/*
For a test vector, we will use the following inputs to the ChaCha20
block function:
o Key = 00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:10:11:12:13:
14:15:16:17:18:19:1a:1b:1c:1d:1e:1f.
o Nonce = (00:00:00:00:00:00:00:4a:00:00:00:00).
o Initial Counter = 1.
We use the following for the plaintext. It was chosen to be long
enough to require more than one block, but not so long that it would
make this example cumbersome (so, less than 3 blocks):
Plaintext Sunscreen:
000 4c 61 64 69 65 73 20 61 6e 64 20 47 65 6e 74 6c Ladies and Gentl
016 65 6d 65 6e 20 6f 66 20 74 68 65 20 63 6c 61 73 emen of the clas
032 73 20 6f 66 20 27 39 39 3a 20 49 66 20 49 20 63 s of '99: If I c
048 6f 75 6c 64 20 6f 66 66 65 72 20 79 6f 75 20 6f ould offer you o
064 6e 6c 79 20 6f 6e 65 20 74 69 70 20 66 6f 72 20 nly one tip for
080 74 68 65 20 66 75 74 75 72 65 2c 20 73 75 6e 73 the future, suns
096 63 72 65 65 6e 20 77 6f 75 6c 64 20 62 65 20 69 creen would be i
112 74 2e t.
Nir & Langley Informational [Page 11]
RFC 7539 ChaCha20 & Poly1305 May 2015
The following figure shows four ChaCha state matrices:
1. First block as it is set up.
2. Second block as it is set up. Note that these blocks are only
two bits apart -- only the counter in position 12 is different.
3. Third block is the first block after the ChaCha20 block
operation.
4. Final block is the second block after the ChaCha20 block
operation was applied.
After that, we show the keystream.
First block setup:
61707865 3320646e 79622d32 6b206574
03020100 07060504 0b0a0908 0f0e0d0c
13121110 17161514 1b1a1918 1f1e1d1c
00000001 00000000 4a000000 00000000
Second block setup:
61707865 3320646e 79622d32 6b206574
03020100 07060504 0b0a0908 0f0e0d0c
13121110 17161514 1b1a1918 1f1e1d1c
00000002 00000000 4a000000 00000000
First block after block operation:
f3514f22 e1d91b40 6f27de2f ed1d63b8
821f138c e2062c3d ecca4f7e 78cff39e
a30a3b8a 920a6072 cd7479b5 34932bed
40ba4c79 cd343ec6 4c2c21ea b7417df0
Second block after block operation:
9f74a669 410f633f 28feca22 7ec44dec
6d34d426 738cb970 3ac5e9f3 45590cc4
da6e8b39 892c831a cdea67c1 2b7e1d90
037463f3 a11a2073 e8bcfb88 edc49139
Keystream:
22:4f:51:f3:40:1b:d9:e1:2f:de:27:6f:b8:63:1d:ed:8c:13:1f:82:3d:2c:06
e2:7e:4f:ca:ec:9e:f3:cf:78:8a:3b:0a:a3:72:60:0a:92:b5:79:74:cd:ed:2b
93:34:79:4c:ba:40:c6:3e:34:cd:ea:21:2c:4c:f0:7d:41:b7:69:a6:74:9f:3f
63:0f:41:22:ca:fe:28:ec:4d:c4:7e:26:d4:34:6d:70:b9:8c:73:f3:e9:c5:3a
c4:0c:59:45:39:8b:6e:da:1a:83:2c:89:c1:67:ea:cd:90:1d:7e:2b:f3:63
Nir & Langley Informational [Page 12]
RFC 7539 ChaCha20 & Poly1305 May 2015
Finally, we XOR the keystream with the plaintext, yielding the
ciphertext:
Ciphertext Sunscreen:
000 6e 2e 35 9a 25 68 f9 80 41 ba 07 28 dd 0d 69 81 n.5.%h..A..(..i.
016 e9 7e 7a ec 1d 43 60 c2 0a 27 af cc fd 9f ae 0b .~z..C`..'......
032 f9 1b 65 c5 52 47 33 ab 8f 59 3d ab cd 62 b3 57 ..e.RG3..Y=..b.W
048 16 39 d6 24 e6 51 52 ab 8f 53 0c 35 9f 08 61 d8 .9.$.QR..S.5..a.
064 07 ca 0d bf 50 0d 6a 61 56 a3 8e 08 8a 22 b6 5e ....P.jaV....".^
080 52 bc 51 4d 16 cc f8 06 81 8c e9 1a b7 79 37 36 R.QM.........y76
096 5a f9 0b bf 74 a3 5b e6 b4 0b 8e ed f2 78 5e 42 Z...t.[......x^B
112 87 4d .M
*/
let le_to_u32 = |x: &[u8]| {
let mut ret = 0u32;
//println!("le is {:x?} ", x);
for x in x.iter().enumerate() {
ret ^= (*x.1 as u32) << (8u32 * x.0 as u32);
}
ret
};
let be_to_u32 = |x: &[u8]| {
let mut ret = 0u32;
for x in x.iter().enumerate() {
ret += (*x.1 as u32) << (8u32 * (4 - x.0 as u32));
}
ret
};
let key = hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
.expect("key is invalid")
.chunks(4)
.map(le_to_u32)
.collect::<Vec<u32>>();
let nonce = hex::decode(b"000000000000004a00000000")
.expect("nonce is invalid")
.chunks(4)
.map(le_to_u32)
.collect::<Vec<u32>>();
let counter = 1u32;
let plain_text = b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
let ans = hex::decode("6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0bf91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d807ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab77937365af90bbf74a35be6b40b8eedf2785e42874d").expect("stream ans is error");
//let ans = ans.chunks(4).map(le_to_u32).collect::<Vec<u32>>();
println!("plain text is {:x?} ", plain_text);
let encode_text = ChaCha::encode(&key, counter, &nonce, plain_text);
println!("encode text is {:x?}", encode_text);
assert_eq!(ans, encode_text);
}
}
ChaChaを用いた乱数生成
さて、ここまで読んでくれたということは、ChaChaか暗号系にかなり興味があるんでしょうね。
...無くても読めるものなんだろうか?
もし無くても、ここまで読んで少しでも興味が出てくれたら嬉しい。
どちらにしても、読んでもらえたなら書いたかいがある!
感謝!
閑話休題
ChaChaを用いた乱数生成の処理について検証していく。
手順は
- ライブラリの内容を把握
- 同様のパラメータで実装&出力
- テストで両方を比較
で行っていく。
参考にしたライブラリはrandcrate。
先に書いておくと、1Block_functionを以下のパラメータで実行する。
key(=seed) =固定の乱数値。バレたら終わりの値
counter = 0
nonce = 0
rand
rustのrand_chachaというクレートが存在するので、そちらの実装を覗き見してみると、ChaCha20Rngというstructが存在する。
randに関してはここを読むと良い。
特にここでChaCha20Rngの説明をしている
読むと、SeedableRngで初期化すると乱数のみ初期化される。
実装を見てみると、seedを入れた場合他を全て0初期化したものと同じみたい。
これは実質counter,nonceをゼロ初期化したものと同義。
サンプルを実行してみる。
use rand::{Rng, SeedableRng};
let seed: [u8; 32] = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
0x1e, 0x1f,
];
let mut ans: ChaCha20Rng = SeedableRng::from_seed(seed);
let ans = (0..16)
.into_iter()
.map(|x| ans.gen::<u32>())
.collect::<Vec<u32>>();
シードはテストベクターに使ったもの、
出力バイト数は1ブロックの16個にしておく。
実装
2つの実装を合わせる。
ライブラリのほうは配列だけど、それ以外は違いなし。
#[test]
fn pseudorandom_test() {
let le_to_u32 = |x: &[u8]| {
let mut ret = 0u32;
//println!("le is {:x?} ", x);
for x in x.iter().enumerate() {
ret ^= (*x.1 as u32) << (8u32 * x.0 as u32);
}
ret
};
use rand::{Rng, SeedableRng};
let seed: [u8; 32] = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
0x1e, 0x1f,
];
let mut ans: ChaCha20Rng = SeedableRng::from_seed(seed);
let ans = (0..16)
.into_iter()
.map(|x| ans.gen::<u32>())
.collect::<Vec<u32>>();
let key = hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
.expect("key is invalid")
.chunks(4)
.map(le_to_u32)
.collect::<Vec<u32>>();
let nonce = hex::decode(b"000000000000000000000000")
.expect("nonce is invalid")
.chunks(4)
.map(le_to_u32)
.collect::<Vec<u32>>();
let counter = 0u32;
let pseudo = ChaCha::block(&key, counter, &nonce);
assert_eq!(ans, pseudo);
}
無事通っている。
逆に言えば初期乱数が分かっていれば...
怖いね。
ちなみにそれら真正乱数はエントロピーと言うものが使用されており、rand_chacha(Linux上)では、/dev/random[2]の乱数をエントロピーとして使用されている。
Linuxでの真正乱数...と言われているけど、/dev/random内部ではChaChaが使われているので、実際はさらに内部の乱数がエントロピーを担当している。
と思いきや、
さらにさらに、それすらもBlake2sハッシュ関数のハッシュで作成されているので、その入力がエントロピーを担当している。
...マトリョーシカかな?
そのエントロピーは、ハードウェアのノイズとかを使用しており、できる限りわかり難くされている。
Linuxの乱数についても調査しており、長くなるので別の記事にする。
まとめ
ChaChaの実装自体はかなり簡単にできた。
データの攪拌部分も複雑ではなく、非常にシンプルなものになっている。
暗号を試しに実装する場合、非常におすすめだと思う!
一応、勘違いしないでほしいのだが、(AESもそうだけど)
簡単に作れる=強度や仕組みが弱い、という意味ではない。
効率的で無駄がないから簡単にすることができている、と考えてほしい。
混合関数に関してもChaChaを基にした暗号系やハッシュはたくさんある。
簡単にランダムな数が生成できるなら使ったほうが良いに決まっているし、そこに古さは関係ない。
ただ欠点もある。
それを参考にして作ったものは、同じ脆弱性を抱える可能性が高くなるという点。
さながら、同じ遺伝子の生物は同じ感染病にかかりやすいように、脆弱性には常に気を付ける必要はある。
...まぁそんなこと作っている人たちはわかっているから、気にしなくても推奨されているやつ使えばいいと思うけどね。
..zennではじめてちゃんと記事書いたけど、悪くないな。
参照文献
感謝!
Discussion
ChaCha20はSSHの通信路の暗号化に使われています。OpenSSHのデフォルトの設定では最も優先される方式となっているので、実際に使われる事も多いでしょう。
またTLSでもサポートされていて、TLS 1.3では実装が必須とされています。
他にはIPSecでも使われているようです。
お~マジですか!助かります!
SSHとTLSで使われているなら十分使われているといっていいですね~
これは恥ずかしいことを書いてしまいました。
まだ現役の暗号化であることを追記します。
てっきりAES-GCMばっかり使われていると見くびってました。
AES-NIがない場合、速度的にも利点があるからかな?
調査不足を以後気を付けます
コメント感謝です!