DIY CLI 暗号化ツール
単体テストにより、私たちのライブラリが正しいことが示されました(少なくとも最初の、バックドアのないバージョンは)。 次はそれを使って、コマンドラインインターフェイス(CLI)の暗号化ユーティリティを構築します!
このツールは 2 つの引数、ファイル名と 16 進数の暗号化キーを受け取り、ディスク上のファイルを暗号化または復号します。
これらの要件は単純なので、Rust の標準ライブラリだけを使って CLI ツールを簡単に構築できます。
-
std::envモジュール1は、他の機能に加えて、OS に依存しない引数アクセス(正確にはパースではありません)を提供します。 -
std::fsモジュール2は、OS に依存しないファイルシステム入出力(例: ファイルの読み書き)を提供します。
厳密に std だけを使って進めるのもよい方法です。
純粋主義者である場合や、手引きなしで問題に取り組みたい場合は、以下のプログラムを標準ライブラリだけを使うように適応してみてもよいでしょう。
ただしここでは、Rust のサードパーティライブラリエコシステムを試してみます。
実世界のコマンドラインアプリケーションでは、さまざまなサードパーティライブラリが使われています。 Rust の CLI エコシステムは、プラグアンドプレイ(構築とリンクが容易)な引数パース3、テキストの色付け4、統合テスト5、テキストベースのユーザーインターフェイス(TUI)6などを提供しています。 コミュニティが保守する膨大なライブラリ群により、CLI アプリの構築と保守は楽しいものになります。
それでは、人気のあるライブラリを試してみましょう。引数パースには clap クレート3を使います。
clap は Rust のマクロシステムを使って宣言的な引数パースロジックを可能にします。これはまもなく確認します。
成長する Rust エコシステムを活用する
Rust の強力な特徴の 1 つは、公式パッケージマネージャー兼ビルドシステムである
cargoです。 私たちはすでにcargoを使って RC4 ライブラリをコンパイルし、テストしました。 しかしcargoの真価は、より広範な Rust エコシステムのライブラリをいかに簡単に活用できるかにあります。現代の開発において、プログラミング言語の実用性は、コア言語機能と、誰かが開発・保守しているドメイン固有の抽象化がライブラリ(Rust では「クレート」と呼ばれます)の形で利用可能であることの両方によって決まります。
ソフトウェアエンジニアは、高品質なコードを素早く出荷する必要があります。 つまり、適切な場合には既存のコードを使ってプロダクトの立ち上げを加速するということです。 この記事の執筆時点で、Rust ライブラリの公式集中リポジトリである crates.io には 75,000 を超えるクレートがホストされています。
もちろん、すべてのクレートが十分に保守されているわけでも、本番品質であるわけでも、安全であるわけでもありません。 しかし、私たちには選択できる多くの選択肢があります。 そしてその数は、Rust エコシステムが成熟するにつれて増え続けるでしょう。
cargo は、clap の最新バージョンのダウンロードとビルドを処理してくれます。
私たちが行う必要があるのは、crypto_tool/rcli/Cargo.toml に 1 行追加することだけです。
[package]
name = "rcli"
version = "0.1.0"
edition = "2021"
# その他のキーとその定義については https://doc.rust-lang.org/cargo/reference/manifest.html を参照してください
[dependencies]
rc4 = { path = "../rc4" }
clap = { version = "^4", features = ["derive"] }
-
features = ["derive"]は、clapライブラリのオプション機能である derive マクロのサポートを有効にします。これにより、いくらかのボイラープレートを省けます。 -
version = "^4"は、このツールが最新のclapバージョン>= 4.0.0かつ< 5.0.0を使うことをcargoに伝えます。Rust クレートはセマンティックバージョニング(semver、例:MAJOR.MINOR.PATCH)7に従うため、4.x.xバージョンでは API の安定性を期待できます。
rc4 依存関係とは異なり、clap にはローカルの path を指定していないことに注目してください。
cargo は、私たちが rcli プロジェクトを初めてビルドまたは実行するときに、crates.io からソースコードをダウンロードします。
サードパーティコードは信頼できるのか?
ほとんどのソフトウェアはサードパーティコードを活用しています。 しかし、取り込む外部コンポーネントはそれぞれ、バグや脆弱性をシステムに持ち込むリスクがあります。 このため、成熟した組織では依存関係やサプライヤーを審査するプロセスが整備されています。
文脈によっては、サードパーティ依存関係のソースを監査し、すべてのビルドで監査済みのバージョンだけを使う方が安全です。 内部リポジトリやビルドシステムの設定は、個々の企業やチームに固有です。
特定の問題クラスでは、サードパーティコードが実際にはリスクを低減することに注意してください。 暗号技術は典型的な例です。同じアルゴリズムを自分たちで実装するより、成熟したライブラリを取り込む方が良い可能性が高いでしょう。
手動の引数パースは C ほど Rust では危険ではありませんが、それでも
clapを使うことでエラーの可能性を減らせます。
clap による引数のパース
clap の最も便利な機能の 1 つは、構造体のフィールドにアノテーションを付けられることです。
それぞれのアノテーションは、機械的には Rust マクロであり、引数の表示とパースを処理するコードを生成します。
-
ユーザーが CLI ツールを呼び出すと、要求された設定/操作を含む単一の構造体(以下の
Args)が得られます。 -
引数パースの複雑な詳細を気にする代わりに、「ビジネスロジック」、つまり要求されたタスクを実行するために構造体のフィールドを処理することに労力を集中できます。
これがどのように機能するか見てみましょう。
以下を crypto_tool/rcli/src/main.rs に追加してください。
use clap::Parser;
/// RC4 file en/decryption
#[derive(Parser, Debug)]
struct Args {
/// Name of file to en/decrypt
#[arg(short, long, required = true, value_name = "FILE_NAME")]
file: String,
/// En/Decryption key (hexadecimal bytes)
#[arg(
short,
long,
required = true,
value_name = "HEX_BYTE",
num_args = 5..=256,
)]
key: Vec<String>,
}
fn main() {
let args = Args::parse();
println!("{:?}", args);
}
-
Argsは 2 つのフィールドを持つstructです。-
fileは、暗号化/復号されるファイルのパス/名前を含む文字列です。 -
keyは、スペース区切りの個々の文字列の動的配列です。各文字列はキーの 16 進数バイトになります。
-
-
clapのフィールドアノテーション(#[something(...)]という形式のマクロ)のハイライト:-
short- 短い引数名(例:fileに対する-f)を生成します。 -
long- 長い引数名(例:fileに対する--file)を生成します。 -
required = true- ツールを実行するには引数を提供しなければならないことを示します。 -
num_args = 5..=256- RC4 の最小 5 バイト(40 ビット)および最大 256 バイト(2048 ビット)のキー長を強制します。
-
現在、2 行の main 関数は、ユーザー入力から収集した Args 構造体を出力するだけです。
フォーマット指定子 {:?} により、デフォルトフォーマッタを使用できます。Args は Debug トレイトを derive しているため、これをサポートしています。
トレイトについては次の章で説明します。
clapがサポートするアノテーションについて知るにはどうすればよいですか?Rustには、組み込みのドキュメントシステムである
rustdoc8 があります。 すべての公開クレートには生成されたドキュメントが提供されていますが、その完全性はプロジェクトによって異なります。clapのドキュメントは https://docs.rs/clap で閲覧できます。
作業中のCLIツールを現状のまま実行するには、コマンド cargo run -- --help を使用できます。
RC4 file en/decryption
Usage: rcli --file <FILE_NAME> --key <HEX_BYTE> <HEX_BYTE> <HEX_BYTE> <HEX_BYTE> <HEX_BYTE>...
Options:
-f, --file <FILE_NAME>
Name of file to en/decrypt
-k, --key <HEX_BYTE> <HEX_BYTE> <HEX_BYTE> <HEX_BYTE> <HEX_BYTE>...
En/Decryption key (hexadecimal bytes)
-h, --help
Print help
-- 区切り記号は、入力の残りをCLIツールに渡すよう cargo に指示します。
この場合、渡しているのはフラグ --help だけです。
便利なことに、clap はこのフラグを私たちのために実装してくれています。
これはCLIツールで一般的な慣習だからです。
Args 構造体の各フィールドに対するコメント(/// で始まる行)が、ヘルプ出力の説明として使用されていることに注目してください。
しかし、--help は main 関数を実行しません。
ツールの通常の使用をシミュレートするために、cargo run -- --file test.txt --key 0x01 0x02 0x03 0x04 0x05 を試してみましょう。
最小の5バイトの鍵長を指定します。
main は次のように出力します。
Args { file: "test.txt", key: ["0x01", "0x02", "0x03", "0x04", "0x05"] }
引数の解析が動作するようになりました!
ファイルの暗号化/復号ロジックの実装
残っているのは、RC4ライブラリと新しいCLIフロントエンドをつなぐ「接着剤」だけです。
main を次のように更新しましょう(先頭に追加されたインポートに注目してください)。
use clap::Parser;
use rc4::Rc4;
use std::fs::File;
use std::io::prelude::{Read, Seek, Write};
// `Args` 構造体は省略、変更なし...
fn main() -> std::io::Result<()> {
let args = Args::parse();
let mut contents = Vec::new();
// Convert key strings to byte array
let key_bytes = args
.key
.iter()
.map(|s| s.trim_start_matches("0x"))
.map(|s| u8::from_str_radix(s, 16).expect("Invalid key hex byte!"))
.collect::<Vec<u8>>();
// Validation note:
// `Args` enforces (5 <= key_bytes.len() && key_bytes.len() <= 256)
// Open the file for both reading and writing
let mut file = File::options().read(true).write(true).open(&args.file)?;
// Read all file contents into memory
file.read_to_end(&mut contents)?;
// En/decrypt file contents in-memory
Rc4::apply_keystream_static(&key_bytes, &mut contents);
// Overwrite existing file with the result
file.rewind()?; // "Seek" to start of file stream
file.write_all(&contents)?;
// Print success message
println!("Processed {}", args.file);
// Return success
Ok(())
}
main に戻り値の型 std::io::Result<()>9 を追加しました。
Rustの Result 型については次の章で扱います。
ここで重要なのは、main の本体内にある、失敗する可能性のあるすべてのファイルI/O操作が ? 演算子で終わっている点です。
これは、操作が失敗した場合に関数を「短絡」させ、即座にエラーの Result を返すよう指示します。
たとえば、誰かがプログラムを実行し、存在しないファイルへのパスを指定したとします。
cargo run -- --file non_existant_file.txt --key 0x01 0x02 0x03 0x04 0x05
let mut file = File::open(args.file)?; の行は失敗し、次のエラーでプログラムを終了します。
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
本番品質のツールでは、エラーを適切に処理したり、ログに記録したり、よりユーザーフレンドリーな出力でラップしたりできます。
エラーが発生しなかった場合は、成功を示すために Ok でラップした空の値(()、Rustのユニット型10)を返すだけです。
新しい main 関数には、説明する価値のある要素がさらにいくつかあります。
-
バッファリングなし: ファイル全体を一度にメモリへ読み込み、バイトベクタ
contentsに格納します。大きなファイルのサポートは、バッファリングと呼ばれる技法を使えばより効率的にできます。これは、一度に小さなチャンクだけを読み込み/暗号化する方法です。この例では、代わりに単純さを目指しています。 -
任意のバイトプレフィックス: 鍵の変換ロジックでは、Rustの関数型スタイルのイテレータを使用しています。イテレータについては後で詳しく説明します。
s.trim_start_matches("0x")により、ユーザーは各バイトにプレフィックス0xを任意で追加できることに注意してください。つまり、--key 01 02 03 04 05も有効で同等の入力になります。 -
入力検証: 自分で制御していない入力を解析するコードを書く場合、その入力は信頼できません。それは攻撃者に制御されている可能性があります。できるだけ早く、つまりプログラムやシステムの他のコンポーネントへ渡す前に検証してください。
mainでは3段階の検証を使用しています。-
有効な鍵長: 暗号化ライブラリが鍵長をチェックすると仮定する代わりに(私たちのRC4実装は実際にチェックします!)、
Argのkeyフィールドにアノテーション(例:num_args = 5..=256)を使用しました。エラーを確認するには、cargo run -- -f anything.txt -k 01を試してください。 -
有効な16進数の鍵バイト:
.expect("Invalid key hex byte!")は、鍵入力にbase-16バイトの無効な文字列表現(例:"0xfg")を受け取った場合にプログラムが投げるエラーメッセージを決定します。 -
必要なファイル権限:
std::fsはOSの機能を使用して、ユーザーが指定されたファイルを読み書きする権限を持っていることを確認します。権限がない場合、プログラムはエラーを投げます。これは先ほど見たError: Os { code: 2, kind: NotFound...のケースと似ています。
-
検証に失敗した場合はどうすべきでしょうか?
それは運用コンテキストによって異なります。 できるだけ早い段階で、対処可能なエラーメッセージとともにCLIツールを終了することで、私たちは攻撃的プログラミングを実践しています。
- 私たちは、早期に失敗することが効果的な最初の防御線であると考えます。特定のエラーを決して許容しないことには利点があると考えています。
ファイル暗号化ロジックがネットワークサービスやプロトコルの一部であった場合、おそらく可用性、つまりシステムをオンラインで到達可能な状態に保つことを優先するでしょう。 防御的プログラミングのほうが適切です。
- エンドユーザーへの影響を最小限に抑えて障害から回復します。
それは、エラー(ステータスコードやプロトコルメッセージを通じて)を返し、すぐに新しいファイル暗号化リクエストの待ち受けに戻ることを意味するかもしれません。 終了する代わりにです。
ツールの使用
まず、このツールを他のコマンドラインユーティリティと同じように使えるようにインストールしましょう。
crypto_tool/rcli ディレクトリから、次を実行します。
cargo install --path .
これで rcli --help を実行できるはずです。
実際にコンパイルされたバイナリがどこにあるかを知りたい場合は、which rcli を実行してください。
ツールを試すために、秘密のメッセージを含むテキストファイルを作成します。
echo "This is a secret, don't tell anyone!" > secret.txt
cat を使って内容を確認できます。
$ cat secret.txt
This is a secret, don't tell anyone!
暗号化後は内容を表示できなくなります。
そのため、今のうちに xxd で見ておきましょう。
$ xxd -g 1 secret.txt
00000000: 54 68 69 73 20 69 73 20 61 20 73 65 63 72 65 74 This is a secret
00000010: 2c 20 64 6f 6e 27 74 20 74 65 6c 6c 20 61 6e 79 , don't tell any
00000020: 6f 6e 65 21 0a one!.
xxd は3つの列を表示しました。
- 左: ファイル内の16進数オフセット。
- 中央: 16進バイトの列として表したファイルの生の内容。
- 右: 生バイトのASCII11デコード(私たちの秘密のメッセージ)。
-g 1 フラグは、その中央の列で各バイトを独立して表示します。
ファイルを暗号化しましょう。
rcli -f secret.txt -k 01 02 03 04 05
出力 Processed secret.txt が表示されるはずです。
もう一度 xxd -g 1 secret.txt を実行すると、次のようになります。
00000000: e6 51 0a 76 d0 54 b3 07 ad e3 21 2f 69 63 7d dc .Q.v.T....!/ic}.
00000010: 45 a2 f0 20 76 db f6 f5 fd a1 6f c8 5a 6c 67 60 E.. v.....o.Zlg`
00000020: d9 e1 1d e3 87 .....
ファイルが暗号化されたため、バイト列が変わりました。
一番右の列は、もはや意味のあるASCII文字列には見えません。
もう一度 rcli を実行して、ファイルを復号し、元のメッセージを取り出せることを確認してください。
このツールは、ファイルを処理するときに「暗号化中」または「復号中」と表示できるでしょうか?
これまで見てきたように、暗号化と復号は実際には同じ操作です。 どちらの場合も、キーストリームとのXORを取っています。 データを隠したのか、それとも明らかにしたのかを示すユーザーフレンドリーなメッセージを表示するには、ファイルの開始時の状態を知る必要があります。
それは簡単な作業ではないことがわかります! 任意のバイト列が暗号化されたファイルであるかどうかを判断するには、「セマンティックギャップ」を埋める必要があります。 それが、この章の課題の一部です。
主要チェックポイント
私たちは、移植性があり、メモリ安全なRC4ライブラリを作成し、公式テストベクターに照らして検証しました。 これは、ベアメタルの組み込みシステムでさえ、どこでも使用できます。
次に、clap クレートとRustの標準ライブラリを活用して、シンプルなRC4 CLIツールを構築しました。
このツールは、主要なOSならどれでもコンパイルできます。
すべてわずか171行のコードです。
すべてのテストを含め、暗号をゼロから実装しています。
そして、そのコードはネイティブにコンパイルされます。rcli は非常に高速です。
初めてのRustプログラムとしては悪くありません。
少し時間を取って、そのことを実感してください。 あなたはすでにかなり遠くまで来ています。 準備ができたら、この章を最後のトピック、運用上の保証で締めくくりましょう。
-
モジュール
std::env. The Rust Team(2022年アクセス)。 ↩ -
モジュール
std::fs. The Rust Team(2022年アクセス)。 ↩ -
クレート
owo-colors. jam1garner(2022年アクセス)。 ↩ -
クレート
assert_cmd. Ed Page(2022年アクセス)。 ↩ -
セマンティック バージョニング 2.0.0. Tom Preston-Werner(2022年アクセス)。 ↩
-
型定義
std::io::Result. The Rust Team(2022年アクセス)。 ↩ -
プリミティブ型 unit. The Rust Team(2022年アクセス)。 ↩