金色の配線パターンが広がる基板の接写
技術解説

Rust製ゼロ知識2FAアプリOtpVaultに学ぶAI駆動開発の落とし穴

目次を見る

AIコーディングツールを使ってセキュリティ関連の個人開発アプリを作っているエンジニアに向けた記事です。とくにRust(メモリ安全性を重視したシステムプログラミング言語)とTauri(RustバックエンドとWebフロントエンドを組み合わせてデスクトップ・モバイルアプリを作るフレームワーク)を使った暗号化アプリの実装で起きがちな問題を扱います。

題材にするのは、開発者が個人で公開したOtpVaultという2FA(二要素認証。ログイン時にパスワードとは別のコードを求める仕組み)認証アプリです。AES-256-GCM(データを暗号化する方式の一つ)やArgon2id(パスワードから暗号鍵を安全に生成する関数)を使い、サーバー側が平文を一切見ないゼロ知識アーキテクチャ(サーバーが利用者の秘密情報を復号できない設計)を謳っています。この種の実装をAI支援で進めるとき、どこでつまずきやすいかを整理します。

何が起きるか:AI生成コードが「動くけど危険」になる

Rust + Tauri + Reactという構成は、ChatGPTやClaudeのようなAIコーディングアシスタントに雛形を書かせやすい組み合わせです。

ただし暗号処理のコードは「コンパイルが通る」「テストがグリーンになる」ことと「安全である」ことが一致しません。たとえばAIに「AES-GCMで暗号化する関数を書いて」と頼むと、nonce(暗号化のたびに変える必要がある使い捨ての値)を固定値やゼロ埋めにしたコードが返ってくることがあります。

動作確認では問題なく暗号化・復号ができてしまうため、レビューで気づかない限り本番に混入します。同じnonceを使い回すとAES-GCMの安全性が崩れ、暗号文から情報が漏れる可能性が出てきます。

OtpVaultのようにvault(暗号化された2FAシークレットの塊)をクラウド同期する設計では、この種のミスが「サーバーが破られても大丈夫」という前提そのものを壊します。

なぜ起きるか:段階的に原因を分解する

原因は一つではなく、複数の層が重なっています。

1. AIの学習データにはアンチパターンも含まれる

大規模言語モデルは公開コードから学習しているため、古いブログ記事やStack Overflowの回答に含まれる「動くが安全ではない」実装パターンも再現します。暗号ライブラリのAPIは正しく呼べていても、呼び出し方の作法(nonceの生成元、鍵の破棄タイミングなど)まで正しいとは限りません。

2. プロンプトが機能要求どまりになりやすい

「2FAシークレットを暗号化保存したい」という指示だけでは、AIは機能を満たす最短ルートを選びます。鍵導出の反復回数(Argon2idのメモリコストやイテレーション回数)やnonceの一意性保証といった非機能要件は、明示しないと抜け落ちます。

3. クロスプラットフォーム実装で暗号フォーマットがずれる

OtpVaultではAndroid版がbase64でsalt+nonce+ciphertextを結合した形式を使い、PWA(ブラウザで動くアプリ)版はvault全体を暗号化する層が別に存在すると説明されています。AIに個別のプラットフォームごとにコードを書かせると、フォーマット変換の実装だけをその場しのぎで追加しがちです。フォーマットの不整合は同期バグの温床になり、最悪の場合は復号に失敗したデータを誤って上書きするリスクにつながります。

自分のプロジェクトが該当するか確認する

該当するかどうかは、次の観点でチェックできます。

  • 暗号化処理(AES-GCM、ChaCha20-Poly1305など)をAIに生成させたコードがリポジトリにあるか
  • grep -rn "nonce" src/grep -rn "IV" src/ でnonce・IVの生成箇所を洗い出し、乱数生成関数(Rustならrand::rngs::OsRngなど)から都度生成しているか確認する
  • Cargo.toml の依存関係で aes-gcmargon2 のバージョンを確認し、公式ドキュメントのnonce取り扱いの注意書きと実装を突き合わせる
  • 複数プラットフォーム(デスクトップ・モバイル・Web)で暗号フォーマットを共有している場合、シリアライズ形式(バイト列の並び順、base64エンコードの有無)が全プラットフォームで一致しているか
# nonce/IV生成箇所の洗い出し例
grep -rn "nonce\|IV\|iv" src/ --include="*.rs"

# 依存クレートのバージョン確認
cat Cargo.toml | grep -A1 "aes-gcm\|argon2"

このコマンドで固定文字列や配列リテラルがnonceとして直接書かれていたら要注意です。乱数生成器を経由せず [0u8; 12] のような初期化がされていないか目視で確認してください。

対策の手順

暗号処理をAI支援で書く場合、次の手順を踏むことでリスクを減らせます。

1. プロンプトに非機能要件を明記する

「AES-256-GCMで暗号化して」ではなく「nonceは暗号化のたびにOsRngで12バイト生成し、平文と一緒に保存すること」まで指定します。Argon2idを使う場合も、メモリコスト・並列度・イテレーション回数を具体的な数値で指示します。

2. 生成コードは暗号ライブラリの公式ドキュメントと突き合わせる

RustのRustCrypto系クレート(aes-gcmクレートなど)はdocs.rsに使用例があります。AIが出したコードとdocs.rsのサンプルを1行ずつ比較し、鍵・nonceの生成元が一致しているか確認します。

3. 暗号処理は専用のレビュー観点でチェックリスト化する

  • nonce/IVは呼び出しごとに新規生成しているか
  • 鍵はメモリ上でゼロクリアされているか(Rustならzeroizeクレートの利用を検討)
  • パスワードから鍵を導出する箇所でsaltがユーザーごとに一意か
  • エラーメッセージに平文や鍵の情報が漏れていないか

4. クロスプラットフォームなら暗号フォーマットを1つの仕様書にまとめる

バイト列のレイアウト(salt長・nonce長・ciphertext)を図か表で明文化し、AIにコードを書かせる際は毎回その仕様書を貼り付けて参照させます。プラットフォームごとに別々のプロンプトセッションで生成すると仕様がずれやすいため、共通の型定義やシリアライズ関数を1箇所にまとめて全プラットフォームから呼ぶ設計が安全です。

AIが書いた暗号コードは「動く」ことと「安全」であることを別軸で検証し、nonce生成とフォーマット統一だけは必ず人間の目で追ってください。

5. 可能なら外部の暗号監査ツールやlintを併用する

Rustであればcargo auditで既知の脆弱性がある依存クレートを検出できます。加えてcargo clippyで明らかなアンチパターンの一部を拾えることもありますが、暗号ロジックの安全性そのものまでは保証しないため、あくまで補助として使います。

cargo audit
cargo clippy -- -W clippy::all

まとめ

AI駆動開発でRustやTauriを使った暗号化アプリを組む際は、機能面のテストが通ることと安全性が別問題である点を意識する必要があります。

  • nonce/IVの生成箇所をgrepで洗い出し、乱数生成器経由になっているか確認する
  • プロンプトには暗号パラメータ(Argon2idの反復回数、nonceの生成方法)まで具体的に指示する
  • クロスプラットフォーム開発では暗号フォーマットの仕様書を1つにまとめ、全プラットフォームで共有する
  • cargo auditのようなツールを補助的に使いつつ、最終的な暗号ロジックは人間がドキュメントと突き合わせて確認する

個人開発でも公開してユーザーに使ってもらう以上、暗号まわりのコードだけはAI任せにせず一手間かけて検証しておくと安心です。

参考

How I Built a Zero-Knowledge 2FA Authenticator Using Rust, Tauri, and React

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。