Programmieren in Rust

Algorithmen: Kryptografie

Inhaltsverzeichnis

  1. Das Konzept der Stromchiffre
  2. ChaCha20
  3. Testvektoren
  4. Literatur

Das Konzept der Stromchiffre

Eine Stromchiffre ist ein durch einen Schlüssel (key) und eine Nonce (number used exactly once) parametrisierter deterministischer kryptografischer Zufallszahlengenerator. Der vom Generator erzeugte geheime Datenstrom heißt Schlüsselstrom. Der Schlüsselstrom muss die gleichen Eigenschaften wie ein One-Time-Pad besitzen und wird wie dieses bitweise mit den Klardaten XOR-verknüpft um das Chiffrat zu erhalten. Zur Entschlüsselung wird diese Operation ein zweites Mal mit gleichem Schlüsselstrom angewandt – die Reinform einer Stromchiffre ist also involutorisch. Wie bei einem One-Time-Pad darf der Schlüsselstrom nur ein einziges Mal benutzt werden.

Die moderne Kryptografie kennt Stromchiffren als Mittel der Wahl. Dies hat die folgenden Gründe:

Zu bemerken ist noch, dass Echtzeitanwendungen und schnelle Datenübertragungen, wie sie gerade im Internet überall vorkommen, eine erhöhte Anforderung an die Implementation einer Chiffre haben. Bei solchen Anwendungen muss das Verfahren vor Seitenkanalattacken wie Rechenzeitattacken (timing attacks) geschützt werden. [1]

Zudem besteht das grundsätzliche Problem, dass aus der Länge des Chiffrats unter manchen Umständen auf die Klardaten geschlossen werden kann. Natürlich ist eine Verschleierung möglich, allerdings verträgt sich dies nicht mit der gewünschten Datensparsamkeit im Internet.

ChaCha20

// Die Stromchiffre ChaCha20 von Daniel Bernstein.

fn u32_from_bytes_le(a: &[u8]) -> u32 {
    u32::from(a[0]) | u32::from(a[1])<<8 |
    u32::from(a[2])<<16 | u32::from(a[3])<<24
}

fn quarter_round(x: &mut [u32; 16],
    a: usize, b: usize, c: usize, d: usize
) {
    x[a] = x[a].wrapping_add(x[b]);
    x[d] = (x[d]^x[a]).rotate_left(16);
    x[c] = x[c].wrapping_add(x[d]);
    x[b] = (x[b]^x[c]).rotate_left(12);
    x[a] = x[a].wrapping_add(x[b]);
    x[d] = (x[d]^x[a]).rotate_left(8);
    x[c] = x[c].wrapping_add(x[d]);
    x[b] = (x[b]^x[c]).rotate_left(7);
}

fn chacha20_stream(key: &[u8; 32], iv: &[u8; 8],
    callback: &mut dyn FnMut(u8) -> bool
) {
    let mut state: [u32; 16] = [0; 16];
    let x: &mut [u32; 16] = &mut [0; 16];

    state[0] = u32_from_bytes_le("expa".as_bytes());
    state[1] = u32_from_bytes_le("nd 3".as_bytes());
    state[2] = u32_from_bytes_le("2-by".as_bytes());
    state[3] = u32_from_bytes_le("te k".as_bytes());
    for i in 0..8 {
        state[4+i] = u32_from_bytes_le(&key[4*i..4*(i+1)]);
    }
    state[12] = 0; // Counter, niederwertige Bits
    state[13] = 0; // Counter, höherwertige Bits
    state[14] = u32_from_bytes_le(&iv[0..4]);
    state[15] = u32_from_bytes_le(&iv[4..8]);

    'stream: loop {
        *x = state;
        for _ in 0..10 {
            quarter_round(x, 0, 4,  8, 12);
            quarter_round(x, 1, 5,  9, 13);
            quarter_round(x, 2, 6, 10, 14);
            quarter_round(x, 3, 7, 11, 15);
            quarter_round(x, 0, 5, 10, 15);
            quarter_round(x, 1, 6, 11, 12);
            quarter_round(x, 2, 7,  8, 13);
            quarter_round(x, 3, 4,  9, 14);
        }
        for i in 0..16 {
            let value = state[i].wrapping_add(x[i]);
            if callback(value as u8) {break 'stream;};
            if callback((value>>8) as u8) {break 'stream;};
            if callback((value>>16) as u8) {break 'stream;};
            if callback((value>>24) as u8) {break 'stream;};
        }
        state[12] = state[12].wrapping_add(1);
        if state[12] == 0 {state[13] += 1;}
    }
}

fn encipher(key: &[u8; 32], iv: &[u8; 8], data: &mut [u8]) {
    let mut i = data.iter_mut();
    chacha20_stream(key, iv, &mut |y| {
        if let Some(x) = i.next() {*x ^= y; false} else {true}
    });
}

fn key_from(text: &str) -> [u8; 32] {
    let mut key: [u8; 32] = [0; 32];
    let n = text.len().min(32);
    key[0..n].clone_from_slice(&text.as_bytes()[0..n]);
    key
}

fn input(prompt: &str) -> String {
    use std::{io,io::Write};
    let mut buffer = String::new();
    print!("{}",prompt);
    io::stdout().flush().unwrap();
    io::stdin().read_line(&mut buffer).unwrap();
    if buffer.ends_with('\n') {
        buffer.pop();
        if buffer.ends_with('\r') {buffer.pop();}
    }
    buffer
}

fn main() -> std::io::Result<()> {
    let argv: Vec<String> = std::env::args().collect();
    let mut data = std::fs::read(&argv[1])?;

    let nonce: [u8; 8] = [0; 8];
    // Eine Nonce wird bei einem Einmalschlüssel nicht benötigt,
    // der dann aber nur ein einziges Mal benutzt werden darf.
    // In der Praxis z.B. mit /dev/urandom initialisieren und
    // öffentlich den verschlüsselten Daten voranstellen.

    let key = key_from(&input("Schlüssel: "));
    // Schlüssel mit mindestens 128 Bit Entropie eintippen,
    // z.B. 28 zufällige Zeichen (Zahlen und kleine Buchstaben).
    // In der Praxis ein aufwändiges Hashverfahren auf die
    // Eingabe anwenden -- da gibt es extra Algorithmen für --
    // und die Nonce als Salt benutzen.

    encipher(&key, &nonce, &mut data);

    std::fs::write(&argv[2], data)?;
    Ok(())
}

Kommt ChaCha20 als kryptografischer Zufallszahlengenerator zur Anwendung, besteht Bedarf an einer externen Iteration. Das führt zu der folgenden Modifikation, wobei der zugrundeliegende Algorithmus identisch bleibt.

struct ChaCha20Stream {
    state: [u32; 16],
    obuff: [u8; 64],
    index: usize
}
impl ChaCha20Stream {
    fn new(key: &[u8; 32], iv: &[u8; 8]) -> Self {
        let mut state: [u32; 16] = [0; 16];
        state[0] = u32_from_bytes_le("expa".as_bytes());
        state[1] = u32_from_bytes_le("nd 3".as_bytes());
        state[2] = u32_from_bytes_le("2-by".as_bytes());
        state[3] = u32_from_bytes_le("te k".as_bytes());
        for i in 0..8 {
            state[4+i] = u32_from_bytes_le(&key[4*i..4*(i+1)]);
        }
        state[12] = 0; // Counter, niederwertige Bits
        state[13] = 0; // Counter, höherwertige Bits
        state[14] = u32_from_bytes_le(&iv[0..4]);
        state[15] = u32_from_bytes_le(&iv[4..8]);
        Self {state, obuff: [0; 64], index: 64}
    }
    fn next_block(&mut self) {
        let mut x: [u32;16] = self.state;
        for _ in 0..10 {
            quarter_round(&mut x, 0, 4,  8, 12);
            quarter_round(&mut x, 1, 5,  9, 13);
            quarter_round(&mut x, 2, 6, 10, 14);
            quarter_round(&mut x, 3, 7, 11, 15);
            quarter_round(&mut x, 0, 5, 10, 15);
            quarter_round(&mut x, 1, 6, 11, 12);
            quarter_round(&mut x, 2, 7,  8, 13);
            quarter_round(&mut x, 3, 4,  9, 14);
        }
        for i in 0..16 {
            let value = self.state[i].wrapping_add(x[i]);
            self.obuff[4*i] = value as u8;
            self.obuff[4*i+1] = (value>>8) as u8;
            self.obuff[4*i+2] = (value>>16) as u8;
            self.obuff[4*i+3] = (value>>24) as u8;
        }
        self.state[12] = self.state[12].wrapping_add(1);
        if self.state[12] == 0 {self.state[13] += 1;}
    }
}

impl Iterator for ChaCha20Stream {
    type Item = u8;
    fn next(&mut self) -> Option<u8> {
        if self.index == 64 {
            self.next_block();
            self.index = 0;
        }
        self.index += 1;
        Some(self.obuff[self.index-1])
    }
}

fn encipher(key: &[u8; 32], iv: &[u8; 8], data: &mut [u8]) {
    let stream = ChaCha20Stream::new(key, iv);
    for (x, y) in data.iter_mut().zip(stream) {
        *x ^= y;
    }
}

Testvektoren

Parameterkey = [0; 32]; iv = [0; 8]
Schlüsselstrom
76 b8 e0 ad a0 f1 3d 90 40 5d 6a e5 53 86 bd 28
bd d2 19 b8 a0 8d ed 1a a8 36 ef cc 8b 77 0d c7
da 41 59 7c 51 57 48 8d 77 24 e0 3f b8 d8 4a 37
6a 43 b8 f4 15 18 a1 1c c3 87 b6 69 b2 ee 65 86
9f 07 e7 be 55 51 38 7a 98 ba 97 7c 73 2d 08 0d
cb 0f 29 a0 48 e3 65 69 12 c6 53 3e 32 ee 7a ed
SHA-2-256
von 1 MiB
fd71 55b0 3a35 4976 e6a9 85c0 f381 d313
b7af 4513 7a51 4ca7 457b 7e76 254f 1a9a

Parameterkey = [0xff; 32]; iv = [0xff; 8]
Schlüsselstrom
d9 bf 3f 6b ce 6e d0 b5 42 54 55 77 67 fb 57 44
3d d4 77 89 11 b6 06 05 5c 39 cc 25 e6 74 b8 36
3f ea bc 57 fd e5 4f 79 0c 52 c8 ae 43 24 0b 79
d4 90 42 b7 77 bf d6 cb 80 e9 31 27 0b 7f 50 eb
5b ac 2a cd 86 a8 36 c5 dc 98 c1 16 c1 21 7e c3
1d 3a 63 a9 45 13 19 f0 97 f3 b4 d6 da b0 77 87
SHA-2-256
von 1 MiB
a826 1c94 10cd 7198 2ea5 372c 3ffa 4e1b
35eb 7391 c9bf 0251 baf0 d7f6 59a6 4760

Parameterkey = b"x152uhlwqtkr4sijepgy3ndmobavfz0c";
iv = b"35270416"
Schlüsselstrom
04 15 c9 98 5f c3 af c9 0b 51 ff 22 52 cc 24 8d
83 3c 2a a6 a0 96 3d 01 25 1e 75 8f bd f4 c0 43
86 67 4a a0 5b 07 db 9e 23 a2 d1 5b cc 9e 37 8a
47 28 aa ca 17 f6 4d 21 1e b1 4a 4f 77 3e 19 ee
4a 62 c3 c2 f7 1a 43 37 c7 0a 8a 72 a6 e6 05 64
3c 5a e4 d2 9b 54 c6 96 d0 ec be 6d 40 23 d0 aa
SHA-2-256
von 1 MiB
1446 5ea6 98b7 29b8 ccda a026 c110 d061
8cbd 888a 0953 7559 57eb 78ef 4a15 cf32

Literatur

  1. Loup Vaillant: »Rolling Your Own Crypto«. Blog-Artikel, Januar 2017.
  2. Fredrik Dahlgren: »Part 1: The life of an optimization barrier«. Blog, 26. Januar 2022.