開発日誌 #3 です。前回はTauriからApple Vision APIを叩いてオフラインOCRを実現する話を書きました。
今回は AES-256-GCM暗号化 と Zero Leak Architecture の設計について書きます。
※検証環境は8年前のMacBook Airです。
設計の前提:なぜ「ゼロ通信」にこだわるのか
PDFツールを作るにあたって、最初に決めた方針があります。
アプリがインターネットに接続するのは、ユーザーが明示的に許可した操作だけ
PDFには契約書・医療記録・確定申告書類など、クラウドに上げたくない情報が含まれることが多い。
なのにバックグラウンドでテレメトリを送ったり、ライセンス確認でサーバーに繋いだりするツールが多すぎる。
Hiyoko PDF Vaultは処理が完全にローカルで完結します。
AES-256-GCM を選んだ理由
PDF暗号化の実装には複数の選択肢がありました。
| 方式 | 認証 | 速度 | 採用 |
|---|---|---|---|
| AES-256-CBC | なし(別途必要) | 速い | ✗ |
| AES-256-GCM | あり(AEAD) | 速い | ✅ |
| ChaCha20-Poly1305 | あり(AEAD) | モバイル向き | ✗ |
GCMは暗号化と認証を同時に行うAEAD方式なので、改ざん検知も込みで処理できます。
CBC単体だと認証が別途必要になり、実装ミスが起きやすい。
Rustでの実装(抜粋)
aes-gcm クレートを使います。
use aes_gcm::{
aead::{Aead, KeyInit, OsRng},
Aes256Gcm, Nonce,
};
use aes_gcm::aead::rand_core::RngCore;
pub fn encrypt_pdf(data: &[u8], key: &[u8; 32]) -> Result, String> {
let cipher = Aes256Gcm::new(key.into());
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, data)
.map_err(|e| e.to_string())?;
// nonce + ciphertext を結合して返す
let mut result = nonce_bytes.to_vec();
result.extend_from_slice(&ciphertext);
Ok(result)
}
pub fn decrypt_pdf(data: &[u8], key: &[u8; 32]) -> Result, String> {
if data.len() < 12 {
return Err("データが短すぎます".to_string());
}
let (nonce_bytes, ciphertext) = data.split_at(12);
let cipher = Aes256Gcm::new(key.into());
let nonce = Nonce::from_slice(nonce_bytes);
cipher
.decrypt(nonce, ciphertext)
.map_err(|_| "復号に失敗しました(パスワードが違うか、ファイルが破損しています)".to_string())
}
Nonceは毎回ランダム生成して先頭12バイトに付加します。
同じファイルを2回暗号化しても異なる出力になるため、パターン解析への耐性が上がります。
パスワードからの鍵導出
ユーザーが入力するパスワードは可変長なので、32バイトの鍵に変換する必要があります。
ここでは Argon2id を使っています。
use argon2::{Argon2, PasswordHasher, password_hash::SaltString};
pub fn derive_key(password: &str, salt: &[u8]) -> [u8; 32] {
let mut key = [0u8; 32];
Argon2::default()
.hash_password_into(password.as_bytes(), salt, &mut key)
.expect("鍵導出に失敗");
key
}
Argon2idはメモリハードな関数なので、ブルートフォース攻撃への耐性が高い。
bcryptやPBKDF2より現代的な選択肢です。
Zero Leak Architecture の実装方針
ネットワーク通信をゼロにするだけでなく、データが意図せず外に出ないための設計を意識しています。
- 一時ファイルは
/tmpではなくNSTemporaryDirectory()配下に作成し、処理後に即削除 - クリップボードにコピーされた機密テキストは30秒後に自動クリア(実装中)
- ログファイルにはファイルパス以外の内容を記録しない
現在の状況(dev版)
パスワードを設定してPDFを暗号化・復号できる画面です。
間違ったパスワードを入力するとGCMの認証タグ検証で即失敗するので、ユーザーにはっきりエラーを返せます。
次回
次回は 1000ページPDFをフリーズなく表示するTurbo View Engine の話を書きます。
仮想化スクロールとGhost Batchの設計について。
Hiyoko PDF Vault(日本語) → https://hiyokoko.gumroad.com/l/HiyokoPDFVault_jp
X → @hiyoyok
