1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RustでAES-256-GCM暗号化を実装し、インターネットに繋がないPDFツールを作る【開発日誌 #3】🐤

1
Last updated at Posted at 2026-04-19

開発日誌 #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版)

Screenshot 2026-04-19 at 12.07.19.png

パスワードを設定してPDFを暗号化・復号できる画面です。
間違ったパスワードを入力するとGCMの認証タグ検証で即失敗するので、ユーザーにはっきりエラーを返せます。


次回

次回は 1000ページPDFをフリーズなく表示するTurbo View Engine の話を書きます。
仮想化スクロールとGhost Batchの設計について。


Hiyoko PDF Vault(日本語) → https://hiyokoko.gumroad.com/l/HiyokoPDFVault_jp
X → @hiyoyok

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?