動機
Windows 環境で MD5 チャックサムを確認する機会があって、ググってみると下記の記事が見つかったので Rust に写経することにしました。(md5 というクレートがあるので普通はそちらを使いましょう。)
環境
Windows 10 21H1
rustc 1.63.0
windows-rs 0.39.0
実装
CryptoAPI
Windows には CryptoAPI という暗号化のためのインターフェースが用意されています。これを使うと MD5 チェックサムができます。
windows クレートは Microsoft 謹製の Rust から WinAPI を呼び出すためのクレートです。まだ破壊的な変更がガンガン入っている段階なので、クレートをアップデートするとビルドエラーなんてことがよくあります。この記事のコードは 2022/09/01 時点の最新版 windows 0.39.0 で動作確認していることにご留意ください。
Cargo.toml
Cargo.toml にクレートを追加します。エラー処理は anyhow に丸投げです。
[dependencies]
anyhow = "1.0"
[dependencies.windows]
version = "0.39"
features = [
"Win32_Foundation",
"Win32_Security_Cryptography",
]
#include, #define
C のヘッダのインクルードと定数定義部分です。#define
は整数の型がわからないのが気持ち悪いですね。バッファサイズが 1KiB ではパフォーマンスが悪かったので Rust ではバッファサイズを 128KiB にしています。
以下 C と Rust のコードを並べて書いていきます。
#include <stdio.h>
#include <windows.h>
#include <Wincrypt.h>
#define BUFSIZE 1024
#define MD5LEN 16
use anyhow::{ensure, Result};
use std::fs::File;
use std::io::{BufReader, Read};
use windows::{core::*, Win32::Security::Cryptography::*};
const BUFSIZE: usize = 1024 * 128;
const MD5LEN: usize = 16;
変数定義
続いて main 関数内です。C は関数の頭でまとめて変数定義しています。私は変数は使用する直前に書くのが好きなので、Rust ではそのように書いています。WinAPI では見慣れない型がよく出てきますが、その型が Rust ではどのサイズか把握しておれば問題ないです。DWORD
ならu32
、BYTE
はu8
、HANDLE
とかH*****
(なにかのハンドル)はisize/usize
(のタプル構造体)、LPC****
(long pointer const)は*const T
のような感じです。
DWORD dwStatus = 0;
BOOL bResult = FALSE;
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
HANDLE hFile = NULL;
BYTE rgbFile[BUFSIZE];
DWORD cbRead = 0;
BYTE rgbHash[MD5LEN];
DWORD cbHash = 0;
CHAR rgbDigits[] = "0123456789abcdef";
LPCWSTR filename=L"filename.txt";
// 例
let mut h_prov: usize = 0;
let mut rgbfile = [0u8; BUFSIZE];
let cb_read: u32 = 0;
let mut rgbhash = [0u8; MD5LEN];
CreateFile, ReadFile
ファイルを読み込む処理です。C のコードをそのまま写経してもよかったのですが、今回は簡単のために Rust の標準ライブラリを使用しました。
hFile = CreateFile(filename,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
if (INVALID_HANDLE_VALUE == hFile)
{
dwStatus = GetLastError();
printf("Error opening file %s\nError: %d\n", filename,
dwStatus);
return dwStatus;
}
// omitted
while (bResult = ReadFile(hFile, rgbFile, BUFSIZE,
&cbRead, NULL))
{
if (0 == cbRead)
{
break;
}
// omitted
}
let file_path = "C:\\test.zip";
let file = File::open(file_path)?;
let mut file = BufReader::new(file);
let mut rgbfile = [0u8; BUFSIZE];
loop {
let cb_read = file.read(rgbfile.as_mut())? as u32;
if cb_read == 0 {
break;
}
// omitted
}
CryptAcquireContext
CryptAcquireContext 関数は CryptoAPI を使用する際に必要なハンドル(HCRYPTPROV)を取得するための関数です。このハンドルは使用が終わったらCryptReleaseContext
で開放しなければなりません。なにかのエラーで関数から抜けるときにこれを書き忘れるとリソース不足につながりますね。Rust では構造体にハンドルを捕捉しておいて、構造体が Drop するときにハンドルが開放されるようにしています。また、エラーで return する処理に anyhow の ensure マクロを使用してます。
if (!CryptAcquireContext(&hProv,
NULL,
NULL,
PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT))
{
dwStatus = GetLastError();
printf("CryptAcquireContext failed: %d\n", dwStatus);
CloseHandle(hFile);
return dwStatus;
}
if // なにかエラーが発生
{
// スコープから抜ける前に HCRYPTPRO を開放しなければならない!
CryptReleaseContext(hProv, 0);
return dwStatus;
}
struct HProve(usize);
impl Drop for HProve {
fn drop(&mut self) {
unsafe {
CryptReleaseContext(self.0, 0);
}
}
}
let mut h_prov = 0;
ensure!(
CryptAcquireContextW(
&mut h_prov,
PCWSTR::null(),
PCWSTR::null(),
PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT,
)
.as_bool(),
"CryptAcquireContext failed. {}",
Error::from_win32()
);
let h_prov = HProve(h_prov);
// スコープを抜けるときに自動的に CryptReleaseContext が呼ばれる
CryptCreateHash
CryptCreateHash 関数はなにやら初期化してハッシュオブジェクトハンドル(HCRYPTHASH)というものを取得するための関数です。このハンドルも使用が終わったらCryptDestroyHash
関数で開放する必要があります。Rust ではこのハンドルも構造体に捕捉しておいて自動的に開放されるようにしておきます。あと、windows クレートにはCALG_MD5
という定数が定義されていないので、自分で定義する必要があります。
if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash))
{
dwStatus = GetLastError();
printf("CryptAcquireContext failed: %d\n", dwStatus);
CloseHandle(hFile);
CryptReleaseContext(hProv, 0);
return dwStatus;
}
// C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\um\wincrypt.h
// #define ALG_CLASS_HASH (4 << 13)
// #define ALG_TYPE_ANY (0)
// #define ALG_SID_MD5 3
// #define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5)
const CALG_MD5: u32 = 4 << 13 | 3;
struct HHash(usize);
impl Drop for HHash {
fn drop(&mut self) {
unsafe {
CryptDestroyHash(self.0);
}
}
}
let mut h_hash = 0;
ensure!(
CryptCreateHash(h_prov.0, CALG_MD5, 0, 0, &mut h_hash).as_bool(),
"CryptCreateHash failed. {}",
Error::from_win32()
);
let h_hash = HHash(h_hash);
HEX で出力
途中ちょっと飛ばしまして、CryptGetHashParam
関数でBYTE rgbHash[MD5LEN]
にハッシュ値を取得できたら、それを HEX で出力する処理です。C では上位 4bit と下位 4bit 取ってきてテーブルを使った変換をしています。Rust では柔軟なフォーマット指定が可能なのでそんなことをする必要もありませんね。
printf("MD5 hash of file %s is: ", filename);
for (DWORD i = 0; i < cbHash; i++)
{
printf("%c%c", rgbDigits[rgbHash[i] >> 4],
rgbDigits[rgbHash[i] & 0xf]);
}
printf("\n");
print!("MD5 hash of file {file_path} is: ");
rgbhash.iter().for_each(|i| print!("{:x}", i));
println!();
大事なことはだいたい書いたのでまとめてどうぞ。
コピペ用サンプルコード
[package]
name = "md5_cryptoapi_sample"
version = "0.1.0"
authors = ["benki"]
edition = "2021"
[dependencies]
anyhow = "1.0"
[dependencies.windows]
version = "0.39"
features = [
"Win32_Foundation",
"Win32_Security_Cryptography",
]
# デバッグビルド高速化のおまじない
[profile.dev]
debug = 0
# 実行速度高速化のおまじない
[profile.release]
lto = "fat"
codegen-units = 1
panic = "abort"
use anyhow::{ensure, Result};
use std::fs::File;
use std::io::{BufReader, Read};
use windows::{core::*, Win32::Security::Cryptography::*};
const BUFSIZE: usize = 1024 * 128;
const MD5LEN: usize = 16;
// C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\um\wincrypt.h
// #define ALG_CLASS_HASH (4 << 13)
// #define ALG_TYPE_ANY (0)
// #define ALG_SID_MD5 3
// #define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5)
const CALG_MD5: u32 = 4 << 13 | 3;
struct HProve(usize);
impl Drop for HProve {
fn drop(&mut self) {
unsafe {
CryptReleaseContext(self.0, 0);
}
}
}
struct HHash(usize);
impl Drop for HHash {
fn drop(&mut self) {
unsafe {
CryptDestroyHash(self.0);
}
}
}
fn main() -> Result<()> {
unsafe {
// Get handle to the crypto provider
let mut h_prov = 0;
ensure!(
CryptAcquireContextW(
&mut h_prov,
PCWSTR::null(),
PCWSTR::null(),
PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT,
)
.as_bool(),
"CryptAcquireContext failed. {}",
Error::from_win32()
);
let h_prov = HProve(h_prov);
let mut h_hash = 0;
ensure!(
CryptCreateHash(h_prov.0, CALG_MD5, 0, 0, &mut h_hash).as_bool(),
"CryptCreateHash failed. {}",
Error::from_win32()
);
let h_hash = HHash(h_hash);
let file_path = "C:\\test.zip";
let file = File::open(file_path)?;
let mut file = BufReader::new(file);
let mut rgbfile = [0u8; BUFSIZE];
loop {
let cb_read = file.read(rgbfile.as_mut())? as u32;
if cb_read == 0 {
break;
}
ensure!(
CryptHashData(h_hash.0, rgbfile.as_ptr(), cb_read, 0).as_bool(),
"CryptHashData failed. {}",
Error::from_win32()
);
}
let mut rgbhash = [0u8; MD5LEN];
let mut cb_hash = MD5LEN as u32;
ensure!(
CryptGetHashParam(
h_hash.0,
HP_HASHVAL.0,
rgbhash.as_mut_ptr(),
&mut cb_hash,
0,
)
.as_bool(),
"CryptGetHashParam failed. {}",
Error::from_win32()
);
print!("MD5 hash of file {file_path} is: ");
rgbhash.iter().for_each(|i| print!("{:x}", i));
println!();
}
Ok(())
}
CNG
Microsoft のドキュメントに書いてある通り、CryptoAPI は廃止予定となっています。「今後は CNG(Cryptography API: Next Generation)を使いなさい」というお達しなので、CNG を使って MD5 チェックサムする C のコードも Rust に写経したので書いておきます。
コピペ用サンプルコード
[package]
name = "md5_cng_sample"
version = "0.1.0"
authors = ["benki"]
edition = "2021"
[dependencies]
anyhow = "1.0"
[dependencies.windows]
version = "0.39"
features = [
"Win32_Foundation",
"Win32_Security_Cryptography",
]
# デバッグビルド高速化のおまじない
[profile.dev]
debug = 0
# 実行速度高速化のおまじない
[profile.release]
lto = "fat"
codegen-units = 1
panic = "abort"
use anyhow::Result;
use std::ffi::c_void;
use std::fs::File;
use std::io::{BufReader, Read};
use std::mem;
use std::ptr;
use windows::{core::*, Win32::Security::Cryptography::*};
struct AlgHandle(BCRYPT_ALG_HANDLE);
impl Drop for AlgHandle {
fn drop(&mut self) {
unsafe {
BCryptCloseAlgorithmProvider(self.0, 0).ok();
}
}
}
struct HHash(*mut c_void);
impl Drop for HHash {
fn drop(&mut self) {
unsafe {
BCryptDestroyHash(self.0).ok();
}
}
}
fn main() -> Result<()> {
unsafe {
//open an algorithm handle
let mut h_alg = BCRYPT_ALG_HANDLE::default();
BCryptOpenAlgorithmProvider(
&mut h_alg,
&HSTRING::from(BCRYPT_MD5_ALGORITHM),
&HSTRING::default(),
BCRYPT_OPEN_ALGORITHM_PROVIDER_FLAGS::default(),
)?;
let h_alg = AlgHandle(h_alg);
//calculate the size of the buffer to hold the hash object
let mut cb_hash_object: u32 = 0;
let mut cb_data = 0;
BCryptGetProperty(
h_alg.0 .0 as *const _,
&HSTRING::from(BCRYPT_OBJECT_LENGTH),
&mut cb_hash_object as *mut u32 as *mut _,
mem::size_of::<u32>() as u32,
&mut cb_data,
0,
)?;
//allocate the hash object on the heap
let mut hash_object = vec![0u8; cb_hash_object as usize];
//calculate the length of the hash
let mut cb_hash: u32 = 0;
BCryptGetProperty(
h_alg.0 .0 as *const _,
&HSTRING::from(BCRYPT_HASH_LENGTH),
&mut cb_hash as *mut u32 as *mut _,
mem::size_of::<u32>() as u32,
&mut cb_data,
0,
)?;
//allocate the hash buffer on the heap
let mut hash = vec![0u8; cb_hash as usize];
//create a hash
let mut h_hash = ptr::null_mut();
BCryptCreateHash(
h_alg.0,
&mut h_hash,
hash_object.as_mut_ptr(),
cb_hash_object,
ptr::null(),
0,
0,
)?;
let h_hash = HHash(h_hash);
//hash some data
let file_path = "C:\\test.zip";
let file = File::open(file_path)?;
let mut file = BufReader::new(file);
let mut buf = [0u8; 1024 * 128];
loop {
let size = file.read(buf.as_mut())? as u32;
if size == 0 {
break;
}
BCryptHashData(h_hash.0, buf.as_ptr(), size, 0)?;
}
//close the hash
BCryptFinishHash(h_hash.0, hash.as_mut_ptr(), cb_hash, 0)?;
print!("MD5 hash of file {file_path} is: ");
hash.iter().for_each(|i| print!("{:x}", i));
println!();
}
Ok(())
}
certutil
Windows では下記のコマンドで MD5 チェックサムできます。Rust に写経したコードでも同じハッシュ値を得られたので、まあ写経したコードは間違っていないということでしょう。
> certutil -hashfile "C:\test.zip" MD5
ベンチマーク
せっかくなので、約 1GB あるファイルのハッシュ値を取得するのに掛かる時間を比較してみました。IO の時間も含んでいます。powershell では以下のコマンドで時間を計測できます。
> powershell -C Measure-Command {certutil -hashfile "C:\test.zip" MD5}
CNG がちょっとだけ早いのかな?
certutil | CryptoAPI | CNG | md5 クレート |
---|---|---|---|
6.91 sec | 5.58 sec | 5.48 sec | 5.90 sec |
md5 クレートのサンプルコード
[package]
name = "md5_crate_sample"
version = "0.1.0"
authors = ["benki"]
edition = "2021"
[dependencies]
anyhow = "1.0"
md5 = "0.7"
[profile.dev]
debug = 0
[profile.release]
lto = "fat"
codegen-units = 1
panic = "abort"
use md5::Context;
use std::fs::File;
use std::io::{BufReader, Read, Result};
fn main() -> Result<()> {
let mut md5 = Context::new();
let file = File::open(r"C:\test.zip")?;
let mut file = BufReader::new(file);
let mut buf = [0u8; 1024 * 128];
loop {
let size = file.read(buf.as_mut())?;
if size == 0 {
break;
}
md5.consume(&buf[0..size]);
}
let digest = md5.compute();
println!("{:?}", digest);
Ok(())
}
まとめ
md5 クレートを使おう