Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第二十九 对称密码算法

对称密码算法可以分为序列密码和分组密码。

序列密码(流密码)

流密码(Stream Cipher) 目前,公开的序列密码算法主要有RC4、SEAL等。常见的使用流密码的加密协议有 RC4 ,Salsa20 ,和 ChaCha 等。祖冲之ZUC 序列密码也称为流密码(Stream Cipher),它是对称密码算法的一种。

ChaCha20-Poly1305 XChaCha20Poly1305

ChaCha20-Poly1305是Google所采用的一种新式加密算法,性能强大.

#![allow(unused)]
fn main() {
    #[test]
    fn chacha20poly1305() {
        use chacha20poly1305::aead::{Aead, NewAead};
        use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
        use data_encoding::HEXLOWER; // Or `XChaCha20Poly1305`
        let key_hex = hex!("98baa9548506c53497bae1b098e85cf26b1359baca7e31ad0c7e93b26e8e79d6");
        let key = Key::from_slice(&key_hex); // 32-bytes
        let cipher = ChaCha20Poly1305::new(key);

        let nonce = Nonce::from_slice(b"unique nonce"); // 12-bytes; unique per message

        let ciphertext = cipher
            .encrypt(nonce, b"plaintext message".as_ref())
            .expect("encryption failure!"); // NOTE: handle this error to avoid panics!

        println!("{}", HEXLOWER.encode(&ciphertext));

        let plaintext = cipher
            .decrypt(nonce, ciphertext.as_ref())
            .expect("decryption failure!"); // NOTE: handle this error to avoid panics!

        assert_eq!(&plaintext, b"plaintext message");
    }
}

XChaCha20Poly1305参考代码

#![allow(unused)]
fn main() {
fn encrypt_large_file(
    source_file_path: &str,
    dist_file_path: &str,
    key: &[u8; 32],
    nonce: &[u8; 19],
) -> Result<(), anyhow::Error> {
    let aead = XChaCha20Poly1305::new(key.as_ref().into());
    let mut stream_encryptor = stream::EncryptorBE32::from_aead(aead, nonce.as_ref().into());

    const BUFFER_LEN: usize = 500;
    let mut buffer = [0u8; BUFFER_LEN];

    let mut source_file = File::open(source_file_path)?;
    let mut dist_file = File::create(dist_file_path)?;

    loop {
        let read_count = source_file.read(&mut buffer)?;

        if read_count == BUFFER_LEN {
            let ciphertext = stream_encryptor
                .encrypt_next(buffer.as_slice())
                .map_err(|err| anyhow!("Encrypting large file: {}", err))?;
            dist_file.write(&ciphertext)?;
        } else {
            let ciphertext = stream_encryptor
                .encrypt_last(&buffer[..read_count])
                .map_err(|err| anyhow!("Encrypting large file: {}", err))?;
            dist_file.write(&ciphertext)?;
            break;
        }
    }

    Ok(())
}

fn decrypt_large_file(
    encrypted_file_path: &str,
    dist: &str,
    key: &[u8; 32],
    nonce: &[u8; 19],
) -> Result<(), anyhow::Error> {
    let aead = XChaCha20Poly1305::new(key.as_ref().into());
    let mut stream_decryptor = stream::DecryptorBE32::from_aead(aead, nonce.as_ref().into());

    const BUFFER_LEN: usize = 500 + 16;
    let mut buffer = [0u8; BUFFER_LEN];

    let mut encrypted_file = File::open(encrypted_file_path)?;
    let mut dist_file = File::create(dist)?;

    loop {
        let read_count = encrypted_file.read(&mut buffer)?;

        if read_count == BUFFER_LEN {
            let plaintext = stream_decryptor
                .decrypt_next(buffer.as_slice())
                .map_err(|err| anyhow!("Decrypting large file: {}", err))?;
            dist_file.write(&plaintext)?;
        } else if read_count == 0 {
            break;
        } else {
            let plaintext = stream_decryptor
                .decrypt_last(&buffer[..read_count])
                .map_err(|err| anyhow!("Decrypting large file: {}", err))?;
            dist_file.write(&plaintext)?;
            break;
        }
    }

    Ok(())
}

}

先压缩,后加密;先解密,后解压;

#![allow(unused)]
fn main() {
    fn compress(source: &str, destination: &str) -> Result<(), anyhow::Error> {
        let source = File::open(source).unwrap();
        let destination = File::create(destination).unwrap();
        match zstd_stream::copy_encode(&source, &destination, 7) {
            Ok(_) => {
                let metadata1 = source.metadata().unwrap();
                if let Ok(metadata2) = destination.metadata() {
                    let size = metadata2.file_size();
                    println!("compress success: {} => {}", metadata1.file_size(), size);
                }
            }
            Err(e) => {
                println!("copy_encode : {}", e)
            }
        }

        Ok(())
    }

    fn decompress(source: &str, destination: &str) -> Result<(), anyhow::Error> {
        // 解压zst文件
        if let Ok(source) = File::open(source) {
            if let Ok(destination) = File::create(destination) {
                zstd_stream::copy_decode(source, destination);
            }
        }

        Ok(())
    }

    #[test]
    fn test_xchacha20_poly1305() {
        use random::{rngs::OsRng, RngCore};
        let mut large_file_key = [0u8; 32];
        let mut large_file_nonce = [0u8; 19];
        OsRng.fill_bytes(&mut large_file_key);
        OsRng.fill_bytes(&mut large_file_nonce);

        //先压缩
        compress("large_file.txt", "large_file.zst");

        //后加密
        encrypt_large_file(
            "large_file.zst",
            "large_file.crypto",
            &large_file_key,
            &large_file_nonce,
        );

        //先解密
        decrypt_large_file(
            "large_file.crypto",
            "large_file.tmp.zst",
            &large_file_key,
            &large_file_nonce,
        );

        //后解压
        decompress("large_file.tmp.zst", "large_file_temp.txt");
    }

}

分组密码

最著名的分组密码是DES密码,而目前最为流行的分组密码算法为AES。

对称加密和分组加密的四种模式:详情

  • ECB模式, 简单,有利于并行计算,误差不会被传递。需要考虑补齐(padding)
  • CBC模式, 密码分组链接模式, 需要引入IV 1.不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL、IPSec的标准。
  • CFB模式, 密码反馈模式
  • OFB模式, 输出反馈模式
  • CTR模式 计数模式 最大的优势是可以并行执行

填充方式

  • NoPadding 不填充,在此填充下原始数据必须是分组大小的整数倍,非整数倍时无法使用该模式

  • ZeroPadding 数据长度不对齐时使用0填充,否则不填充。

  • PKCS1Padding 该填充模式是 RSA 加密中使用的,详见 RFC 2313。RSA 加密时,需要将原文填充至密钥大小,填充的格式为: 00 + BT + PS + 00 + D

  • PKCS5Padding

  • Pkcs7 (the default) 假设数据长度需要填充n(n>0)个字节才对齐,那么填充n个字节,每个字节都是n;如果数据本身就已经对齐了,则填充一块长度为块大小的数据,每个字节都是块大小。

  • Iso97971

  • AnsiX923 填充至符合块大小的整数倍,填充值最后一个字节为填充的数量数,其他字节填 0

  • Iso10126 填充至符合块大小的整数倍,填充值最后一个字节为填充的数量数,其他字节随机处理

PKCS5Padding 的块大小应为 8 个字节,而 PKCS7Padding 的块大小可以在 1~255 的范围内。但 SunJCE 的 Provider 实现中 PKCS5Padding 也按 PKCS7Padding 来进行处理了。

CryptoJS默认模式为CBC模式,采用Pkcs7填充方式。

国密算法介绍及OpenSSL实现

AES

AES的明文分组长度为128位(16字节),密钥长度可以为128位(16字节)、192位(24字节)、256位(32字节),根据密钥长度的不同,AES分为AES-128、AES-192、AES-256三种。

#![allow(unused)]
fn main() {
    #[test]
    fn aes_gcm() {
        use aes_gcm::aead::{Aead, NewAead};
        use aes_gcm::{Aes256Gcm, Key, Nonce};
        use data_encoding::HEXLOWER;
        // Or `Aes128Gcm`
        // 256 bits(32 bytes) key
        // openssl rand -hex 32
        // hex!() : converting hexadecimal string literals to a byte array
        let key = Key::from_slice(&hex!(
            "c2c567b1151904db13374ea7aef181a4b8509e331a7d6e952a11781d29ebfe52"
        ));
        let cipher = Aes256Gcm::new(key);

        let nonce = Nonce::from_slice(b"unique nonce"); // 96-bits; unique per message

        let ciphertext = cipher
            .encrypt(nonce, b"plaintext message".as_ref())
            .expect("encryption failure!"); // NOTE: handle this error to avoid panics!

        println!("{}", HEXLOWER.encode(&ciphertext));
        println!("{}", encode(&ciphertext));
        let plaintext = cipher
            .decrypt(nonce, ciphertext.as_ref())
            .expect("decryption failure!"); // NOTE: handle this error to avoid panics!

        assert_eq!(&plaintext, b"plaintext message");

        //
        match File::open("C:\\data\\寒窑赋.txt") {
            Ok(f) => {
                let mut reader = BufReader::new(f);
                let ciphertext = cipher
                    .encrypt(nonce, reader.fill_buf().unwrap())
                    .expect("encryption failure!");
                println!("{:?}", encode(&ciphertext));

                let plaintext = cipher
                    .decrypt(nonce, ciphertext.as_ref())
                    .expect("decryption failure!");
                println!("{}", String::from_utf8(plaintext).unwrap());
            }
            Err(e) => println!("{}", e),
        }
    }
}

SM4

ISO/IEC 18033-3:2010/Amd 1:2021 SM4分组密码算法正式成为ISO/IEC国际标准

#![allow(unused)]
fn main() {
extern crate rand as random;
fn rand_block() -> [u8; 16] {
    use random::prelude::*;
    // let mut rng = OsRng::new().unwrap();
    let mut rng = random::thread_rng();
    let mut block: [u8; 16] = [0; 16];
    rng.fill_bytes(&mut block[..]);

    println!("block:{}", HEXLOWER.encode(&block));
    block
}

    let key: [u8; 16] = [
        0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32,
        0x10,
    ];

    //SM4
    let cipher = Cipher::new(&key, Mode::Cbc).unwrap();

    let iv = rand_block();
    let poem = String::from("断头今日意如何?创业艰难百战多。此去泉台招旧部 ,旌旗十万斩阎罗。");
    let encrypt_bytes = cipher.encrypt(&poem.as_bytes(), &iv).unwrap();
    println!("{}", base64::encode(&encrypt_bytes));

    let mut all_bytes = Vec::<u8>::with_capacity(128);
    all_bytes.extend_from_slice(&key);
    all_bytes.extend_from_slice(&iv);
    all_bytes.extend_from_slice(&encrypt_bytes);

    std::fs::write("poem.crypto", &all_bytes);

    if let Ok(cipher_data) = std::fs::read("poem.crypto") {
        let cipher = Cipher::new(&cipher_data[0..16], Mode::Cbc).unwrap();
        let poem_bytes = cipher
            .decrypt(&cipher_data[32..], &cipher_data[16..32])
            .unwrap();
        let poem = String::from_utf8(poem_bytes).unwrap();
        println!("poem.crypto => {}", poem);
    }
}
#![allow(unused)]
fn main() {
use std::fs;

#[test]
fn sm4(){
    let key = rand_block();
    let cipher = Cipher::new(&key, Mode::Cbc).unwrap();

    let iv = rand_block();
    
    if let Ok(poem) = fs::read("why-rust.txt"){
        let encrypt_bytes = cipher.encrypt(&poem, &iv).unwrap();
        println!("{}", base64::encode(&encrypt_bytes));
    }
}

}

OpenSSL

openssl enc -sm4-ctr -pbkdf2 -e -in 银行卡.txt -out bank.txt -a
openssl enc -sm4-ctr -pbkdf2 -d -in bank.txt -out decode.txt -a
openssl enc -e -sm4 -in /tmp/1.txt -out /tmp/2.txt