Rust研究：ファイル暗号化、パスワード入力、鍵生成

Rust初心者です。Rust のプログラミング歴一か月です。
ファイルの暗号化、エコーバックなしでのパスワード入力、鍵生成の方法について調べてみました。
また、io::copy を使ったストリーム間コピーの作例が少ないような気がしたので使ってみました。
とりあえずコンパイルが通るところまでできたのでメモしておきます。

# お題

* パスワードの文字列でファイルの暗号化・復号を行うツールがほしい
* 暗号アルゴリズムは AES (Advanced Encryption Standard) を使いたい
* 鍵はユーザに入力させたパスワード文字列より PBKDf2 (Password-Based Key Derivation Function 2) で生成したい
* ユーザにパスワードを入力させる際にはショルダーハック対策として画面にパスワードをエコーバックさせない

使い方：

```
// 暗号化
C:\work> study0215.exe enc plain.png encrypted.dat
ENTER PASSORD:  ←パスワードを入力

// 復号
C:\work> study0215.exe dec encrypted.dat decrypted.png
ENTER PASSORD:  ←パスワードを入力
```

# ソースコード

Cargo.toml
```
[package]
name = "study0215"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rpassword = "7.2"
rust-crypto = "^0.2"
aes-stream = "0.2.1"
```

```rust
// lib.rs
pub mod enc;
pub mod passwd;
pub mod error;
pub mod conf;
```

```rust
// enc.rs
use std::io;
use std::fs::File;
use aesstream::{AesReader, AesWriter};
use crypto::aessafe::{AesSafe256Encryptor, AesSafe256Decryptor};

use crypto::hmac::Hmac;
use crypto::mac::Mac;
use crypto::pbkdf2;
use crypto::sha2::Sha256;

use crate::error::ErrorCode;

const KEY_SIZE:usize = 32;
const SALT:&[u8] = b"Salt";
const ROUNDS:u32 = 10;

// ファイルコピー
pub fn copy(src_file:&str, dst_file:&str) -> Result<u64, ErrorCode> {
    match _copy(src_file, dst_file) {
        Ok(n) => Ok(n),
        Err(e) => Err(ErrorCode::IOError(format!("{:?}", e)))
    }
}
fn _copy(src_file:&str, dst_file:&str) -> io::Result<u64> {
    //println!("{}->{}", src_file, dst_file);
    let mut r = File::open(src_file)?;
    let mut w = File::create(dst_file)?;
    io::copy(&mut r, &mut w)
}

// 暗号化
pub fn encrypt(password:&str, src_plain_file:&str, dst_enc_file:&str) -> Result<u64, ErrorCode> {
    match _encrypt(password, src_plain_file, dst_enc_file) {
        Ok(n) => Ok(n),
        Err(e) => Err(ErrorCode::IOError(format!("{:?}", e)))
    }
}
fn _encrypt(password:&str, src_plain_file:&str, dst_enc_file:&str) -> io::Result<u64> {
    let password = password.as_bytes();
    let mut key = [0u8; KEY_SIZE];
    gen_key(&password[..], &mut key[..]);

    let mut r = File::open(src_plain_file)?;
    let w = File::create(dst_enc_file)?;

    let encryptor = AesSafe256Encryptor::new(&key);
    let mut w = AesWriter::new(w, encryptor)?;

    io::copy(&mut r, &mut w)
}

// 復号
pub fn decrypt(password:&str, src_enc_file:&str, dst_plain_file:&str) -> Result<u64, ErrorCode> {
    match _decrypt(password, src_enc_file, dst_plain_file) {
        Ok(n) => Ok(n),
        Err(e) => Err(ErrorCode::IOError(format!("{:?}", e)))
    }
}
fn _decrypt(password:&str, src_enc_file:&str, dst_plain_file:&str) -> io::Result<u64> {
    let password = password.as_bytes();
    let mut key = [0u8; KEY_SIZE];
    gen_key(&password[..], &mut key[..]);

    let r = File::open(src_enc_file)?;
    let mut w = File::create(dst_plain_file)?;

    let decryptor = AesSafe256Decryptor::new(&key);
    let mut r = AesReader::new(r, decryptor)?;

    io::copy(&mut r, &mut w)
}

// 鍵生成
fn gen_key(password:&[u8], out: &mut [u8]) {
    let mut password = Hmac::new(Sha256::new(), password);
    password.reset();
    pbkdf2::pbkdf2::<Hmac<Sha256>>(&mut password, SALT, ROUNDS, out);
}
```

```rust
// passwd.rs
use rpassword;
use crate::error::ErrorCode;

// prompt_passwd はプロンプトを stdout に出力しパスワードを stdin から読みだす関数
pub fn prompt_passwd(msg:&str) -> Result<String, ErrorCode> {

    let msg = format!("{}: ", msg);
    let mut password:&[u8] = &[];
    let mut s:String;

    while password.len() < 1 {
        s = match rpassword::prompt_password(&msg) {
            Ok(s) => s,
            Err(e) => return Err(ErrorCode::RuntimeError(format!("{:?}", e)))
        };
        password = s.as_bytes()
    }

    match String::from_utf8(password.to_vec()) {
        Ok(x) => Ok(x),
        Err(e) => Err(ErrorCode::RuntimeError(format!("{:?}", e)))
    } 
}
```

```rust
// conf.rs
use std::env;
use crate::error::ErrorCode;

// 設定
pub struct Conf {
    op: Op,
    src_file: String,
    dst_file: String,
}

#[derive(PartialEq)]
enum Op {
    Enc,Dec,
}

impl Conf {
    pub fn new () -> Result<Self, ErrorCode> {

        // 引数をチェック
        let args:Vec<String> = env::args().collect();
        if args.len() < 4 {
            return Err(ErrorCode::ArgumentError)
        }
        let op = match args[1].as_str() {
            "enc" => Op::Enc,
            "dec" => Op::Dec,
            _ => return Err(ErrorCode::ArgumentError)
        };

        Ok(Conf{
            op: op,
            src_file: String::from(&args[2]),
            dst_file: String::from(&args[3]),
        })
    }

    pub fn src_file(&self) -> &str {
        &self.src_file
    }

    pub fn dst_file(&self) -> &str {
        &self.dst_file
    }

    pub fn is_enc(&self) -> bool {
        self.op == Op::Enc
    }
    pub fn is_dec(&self) -> bool {
        self.op == Op::Dec
    }
}
```

```rust
// error.rs
use std::process::{Termination,ExitCode};

pub enum ErrorCode {
    Success,
    ArgumentError,
    IOError(String),
    RuntimeError(String),
}

impl ErrorCode {
    pub fn to_value(&self) -> u8 {
        match self {
            ErrorCode::Success => 0,
            ErrorCode::ArgumentError => 10,
            ErrorCode::IOError(_) => 20,
            ErrorCode::RuntimeError(_) => 30,
        }
    }

    pub fn to_string(&self) -> String {
        match self {
            ErrorCode::Success => "Success".to_string(),
            ErrorCode::ArgumentError => "[Usage] study0215.exe [enc|dec] src_file dst_file".to_string(),
            ErrorCode::IOError(msg) => format!("[IO Error] {}", msg),
            ErrorCode::RuntimeError(msg) => format!("[Runtime Error] {}", msg),
        }
    }

}

// ErrorCode に Termination トレイトを実装する
impl Termination for ErrorCode {
    fn report(self) -> ExitCode {
        ExitCode::from(self.to_value())
    }
}
```

```rust
// main.rs
use study0215::enc::{copy, encrypt, decrypt};
use study0215::error::ErrorCode;
use study0215::conf::Conf;
use study0215::passwd::prompt_passwd;

fn main() -> ErrorCode {
    let conf = match Conf::new() {
        Ok(c) => c,
        Err(e) => return console(e)
    };
    match run(conf) {
        Err(e) => console(e),
        _ => ErrorCode::Success
    }
}

fn console(e:ErrorCode) -> ErrorCode {
    match e {
        ErrorCode::Success => {},
        _ => eprintln!("[ERROR] {}", e.to_string())
    };
    e
}

fn run(conf:Conf) -> Result<(), ErrorCode> {
    // 暗号化・復号前のファイルを別名保存
    //copy(&conf.src_file(), &format!("{}.ORG", conf.src_file()))?;

    // パスワード入力
    let password = prompt_passwd("ENTER PASSWORD")?;
    if conf.is_enc() {
        // 暗号化
        encrypt(&password, &conf.src_file(), &conf.dst_file())?;
    } else {
        // 復号
        decrypt(&password, &conf.src_file(), &conf.dst_file())?;
    }

    Ok(())
}
```

ここまでの感想。

* Rust は探せば暗号処理のライブラリがたくさん見つかることがわかった。
* ただ、暗号処理のサンプルコードとしては小さな文字列を暗号化するものばかりで、なぜか大きなファイルを処理する例は見つからなかった。
* 今回使用した aesstream ライブラリでは暗号利用モードとか IV とかパディングとか指定する必要がないが、ソースコードによると CBC モード、IV は乱数による自動生成、パディングは Pkcs らしい。
* std:: io::copy を使ってみたが、BufReader/BufWriter も併用した方がパフォーマンスはいいのかもしれない。
* パスワード入力のところは rpassword ライブラリを使用。パスワード入力時にエコーバックしないので少し不安になる。替わりに "*" を表示させたいがやり方はまだわからない。