1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

生成AIへ貼る前に機密情報を仮名化・復元するWebツールを作った

1
Posted at

ChatGPTやClaude、Geminiなどの生成AIに文章を整えてもらったり、ログの内容を相談したりする機会が増えました。

一方で、元の文章に氏名、メールアドレス、電話番号、IPアドレス、APIキーなどが含まれていると、そのまま貼り付けることには不安があります。手作業で伏字にすることもできますが、AIの回答に含まれる伏字を元へ戻す作業まで考えると面倒です。

そこで、生成AIへ渡す前の仮名化と、回答を受け取った後の復元をブラウザ内で行うWebツール「Mask & Unmask」を開発し、個人開発ブランド「デジガーデン」から公開しました。

私は「ぎりぎりSE」という名前で技術記事を書きながら、個人開発したサービスを「デジガーデン」名義で公開しています。この記事では、Mask & Unmaskの機能紹介だけでなく、ブラウザ内完結をどう実装したか、仮名化と復元をどう設計したかを中心に紹介します。

なぜ作ろうと思ったか

きっかけは、取得したcircle-note.comドメインを十分に活用できていなかったことでした。

せっかく取得したドメインを何かに活かせないか生成AIに相談したところ、ドメイン名の意味やイメージを起点に、いくつかのサービス案が出てきました。その中の一つが、文章中の情報を仮名化し、あとから元へ戻せる「Mask & Unmask」でした。

ちょうどその頃、AIを開発工程に取り入れた個人開発を実際に経験してみたいと考えていました。単に生成AIにコードを書かせるのではなく、要件整理、設計、実装、テスト、セキュリティ確認、リリースまでを一通り進め、AIをどこで活用し、どこを人が判断すべきか学ぶための題材を探していました。

Mask & Unmaskは機能の目的が明確でありながら、文字列検出、置換、状態管理、ファイル入出力、セキュリティヘッダー、E2Eテストなど、Webアプリ開発の基礎を幅広く扱えます。また、今後デジガーデンから提供していくサービスの最初の一つとしても、規模がちょうどよいと考え、開発を始めました。

アイデアのきっかけには生成AIを使いましたが、提案をそのまま実装したわけではありません。利用場面やリスクを整理し、「入力内容をアプリから外部送信しない」「自動検出を過信させない」「AIの回答から元の表記へ復元できる」といった方針を決めたうえで、要件と実装へ落とし込みました。

Mask & Unmaskでできること

基本的な流れは次のとおりです。

  1. 文章、ログ、コードなどを貼り付ける
  2. 自動検出された情報と置換マップを確認する
  3. 必要な文字列をカスタム登録する
  4. 仮名化した文章だけを生成AIへ渡す
  5. AIの回答を復元画面へ貼り付ける
  6. 仮名タグを元の情報へ戻す

例えば、次の文章を入力します。

株式会社みらいサンプル
山田太郎様

ご質問は sato@example.com または 03-1234-5678 までお願いします。

会社名と氏名をカスタム登録すると、メールアドレスと電話番号は自動検出され、おおむね次のような文章になります。

[CN_CUSTOM_0001]
[CN_CUSTOM_0002]様

ご質問は [CN_EMAIL_0001] または [CN_PHONE_0001] までお願いします。

image.png
上の例では、会社名と氏名をカスタム登録し、メールアドレスと電話番号を自動検出しています。

この文章を生成AIへ渡し、回答に同じタグが残っていれば、Mask & Unmaskで元の会社名、氏名、メールアドレス、電話番号へ復元できます。
image.png
生成AIの回答を貼り付けると、置換マップに基づいて4件の仮名タグが元の情報へ復元されました。置換マップに存在しない未復元タグも確認できます。

技術構成

バックエンドを持たない静的なReactアプリです。

用途 技術
UI React 18 / TypeScript
ビルド Vite
スタイル Tailwind CSS
単体・コンポーネントテスト Vitest / Testing Library / jsdom
E2Eテスト Playwright(Chromium)
CI GitHub Actions
ホスティング Cloudflare Pages

ユーザー登録、ログイン、バックエンドAPI、データベースはありません。仮名化、復元、置換マップの入出力はクライアント側のTypeScriptで処理します。

AIではなくルールベースで検出する

機密情報の抽出自体にAIは使用していません。正規表現と明示的な競合規則による、決定的な処理にしています。

自動検出するカテゴリーは次の5種類です。

  • APIキー:OpenAIの一部形式、AWS Access Key ID、GitHub Personal Access Token
  • IPv4:ポート番号やCIDRを含む形式にも対応
  • IPv6:省略形、角括弧、ゾーンID、ポートを含む形式にも対応
  • メールアドレス:ASCII向けの簡易パターン
  • 日本の電話番号:+81表記を含む

氏名、会社名、案件名など、汎用的な正規表現で扱いにくい値は、ユーザーが選択してカスタム登録できます。

ルールベースを選んだ理由

このツールで重視したのは、検出対象を外部へ渡さず、同じ入力に対して予測可能な結果を返すことです。

AIによる固有表現抽出を採用すると、モデルや外部APIへの入力送信が必要になったり、出力が非決定的になったりします。そこで、自動検出できる範囲を限定し、検出結果を利用者が置換マップで確認・削除できる設計にしました。

もちろん、ルールベースですべての個人情報や秘密情報を検出できるわけではありません。自動検出は補助機能であり、生成AIへ渡す前の目視確認は必要です。

仮名化処理の流れ

仮名化エンジンでは、次の順番で処理します。

入力
  ↓
既存のCNタグを検査
  ↓
登録済みマッピングの値を検索
  ↓
有効なカテゴリーを正規表現で自動検出
  ↓
一時除外された候補を除外
  ↓
重複・部分一致の競合を解決
  ↓
既存タグを再利用、またはカテゴリー別に採番
  ↓
位置情報を使って文章を一度だけ再構築

タグは次の形式です。

[CN_CATEGORY_0001]

番号はカテゴリーごとに独立した4桁連番です。同じ値が文章内に複数回登場した場合は、同じタグを再利用します。

候補が重なった場合

複数の候補が同じ範囲に重なった場合は、次の優先順位で1つに決めます。

  1. ユーザー登録またはインポートした値を自動検出より優先
  2. より長い文字列を優先
  3. カテゴリーの優先順位で決定
  4. より左にある候補を優先

また、入力に既存のCNタグが含まれていた場合は、二重仮名化を避けるため処理全体を中断します。

復元は単純なreplaceの繰り返しにしない

復元画面では、AIの回答からタグ候補を位置情報付きで抽出し、標準化したタグを置換マップと照合します。

AIの回答
  ↓
タグ候補を抽出
  ↓
空白を除去し、ASCIIを大文字化
  ↓
置換マップと照合
  ├─ 一致する → 元の値へ復元
  └─ 一致しない → 未復元タグとして表示
  ↓
位置情報を使って文章を一度だけ再構築

文字列のreplaceをマッピング数だけ繰り返す方式ではなく、元の入力上の位置を基準に一度だけ再構築しています。これにより、復元した元データの中にタグのような文字列が含まれていても、続けて置換されることを防げます。

復元時には、タグ内のASCII大文字・小文字と、構成要素間に入った空白を許容します。ただし、AIがタグを削除したり、ハイフンへ書き換えたり、別の既存タグへ入れ替えたりした場合まで完全に検出できるわけではありません。

実際にGeminiで試した際には、Markdownの都合でタグ周辺にバックスラッシュが付くケースがありました。タグ部分を正規表現で認識できる場合は復元できますが、バックスラッシュを除去する専用処理はありません。生成AIがタグ形式を壊す可能性は、利用上の制約として考える必要があります。

ブラウザ内完結を実装とCSPの両方で担保する

「外部送信しません」と表示するだけでは不十分なので、アプリの構成と配信時の制約の両面で対策しました。

アプリ側

  • 仮名化・復元はTypeScriptによるローカルの文字列処理
  • バックエンドAPI、DB、認証機能なし
  • アプリコードにfetch、XHR、WebSocket、外部APIクライアントなし
  • Analytics SDK、外部フォント、CDNなし
  • クリップボードは出力の書き込みだけで、読み取りは行わない

Content Security Policy

Cloudflare Pagesで、次のCSPを含むセキュリティヘッダーを設定しています。

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'none'; font-src 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'none'

特にconnect-src 'none'によって、アプリ起動後のFetch、XHR、WebSocketなどをブラウザ側でも禁止しています。

ほかにも、次のヘッダーを設定しています。

X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer
Cache-Control: public, max-age=0, must-revalidate, no-transform

これらの必須ヘッダーは、ビルド工程でもスクリプトによって存在・値・重複を検証しています。E2Eテストでは外部URLへのfetchを意図的に実行し、CSPによって遮断されることも確認します。

なお、ページを表示するためのHTML、JavaScript、CSS、画像の取得では、当然ながらホスティング元との通信が発生します。「外部送信しない」とは、入力内容や置換マップをMask & Unmaskから外部へ送信しない、という意味です。

状態はsessionStorageだけに保存する

入力途中でタブを切り替えても作業を続けられるよう、次の状態をsessionStorageへ保存します。

  • 仮名化元の入力
  • 復元元の入力
  • 置換マップ
  • 自動検出カテゴリーのON/OFF
  • 選択中のタブ

Cookie、localStorage、IndexedDBはアプリ実装では使用していません。保存データは読み込み時に型、未知フィールド、タグ形式、件数、文字数などを検証し、不正なら削除して初期状態へ戻します。

「共通クリア」を実行すると、入力、置換マップ、設定、sessionStorageをまとめて消去します。

ただし、sessionStorageは「絶対に復元されない揮発領域」ではありません。ブラウザのセッション復元やタブ複製によって残る場合があります。また、自動的な有効期限は設けていないため、利用後は共通クリアを実行する運用を推奨しています。

置換マップをJSONで持ち運ぶ

長い作業をまたいで置換マップを使いたい場合に備えて、JSONのエクスポート/インポート機能を用意しました。

{
  "version": "1.0",
  "createdAt": "2026-08-13T00:00:00.000Z",
  "mappings": [
    {
      "tag": "[CN_EMAIL_0001]",
      "original": "user@example.com",
      "category": "EMAIL"
    }
  ]
}

インポート時には、バージョン、日時、未知フィールド、タグ形式、カテゴリー整合性、文字数、件数、ファイルサイズなどを検証します。同じタグが別の値に使われている場合は再採番し、適用前にマージ結果をプレビューします。

重要なのは、このJSONには元の機密情報が平文で含まれることです。エクスポート前に警告を表示していますが、ファイルの保管や削除は利用者自身で行う必要があります。

テスト

現時点では、Vitestによるテストが71件、PlaywrightによるE2Eテストが6件あります。

主なテスト対象は次のとおりです。

  • APIキー、IPv4、IPv6、メール、電話番号の検出境界
  • 候補の競合優先度とタグ採番上限
  • 不正JSON、未知フィールド、件数・文字数・ファイルサイズ上限
  • インポート時の重複・タグ衝突とロールバック
  • sessionStorageの破損データ、容量超過、保存失敗
  • クリップボード権限拒否時のフォールバック
  • MaskからUnmaskまでの一連のE2Eフロー
  • CSPによる外部通信の遮断
  • 配信ヘッダーの欠落・重複

100KBの入力を対象にした性能テストも設けています。ただし、測定値は実行環境に依存するため、特定環境での結果を一般的な性能保証とはしていません。

作って分かった制約

この仕組みは、生成AIへ機密情報を渡すリスクを下げるための補助ツールです。完全な情報漏えい防止を保証するものではありません。

特に、次の点には注意が必要です。

  • 自動検出されない情報は、カスタム登録または手作業で確認する必要がある
  • AIがタグを削除した場合、その削除を検出できない
  • AIが既存の別タグへ入れ替えた場合、意味的な改変を検出できない
  • タグ形式が壊れると、未復元タグとしても認識できない場合がある
  • エクスポートJSONには元データが平文で入る
  • sessionStorageがブラウザのセッション復元で残る場合がある
  • 法令、契約、社内規程への適合を保証するものではない

最終的には、仮名化結果を生成AIへ渡す前と、復元結果を利用する前の目視確認が必要です。

まとめ

Mask & Unmaskでは、次の方針で生成AI利用前後の仮名化・復元を実装しました。

  • ルールベースの決定的な検出
  • ユーザーが確認・修正できる置換マップ
  • AIの回答を元の表記へ戻せる双方向変換
  • バックエンドを持たないブラウザ内処理
  • connect-src 'none'による外部通信の制限
  • sessionStorageに限定した状態保持
  • ビルド検証とE2Eを含むテスト

生成AIを使う際の「毎回手作業で伏字にして、回答後に元へ戻す」という手間を少しでも減らせればと思っています。

サービスは無料で利用できます。自動検出できない形式や、生成AIによるタグの書き換えなど、改善の余地もあります。使ってみて気づいた点があれば、フィードバックをいただけるとうれしいです。

1
2
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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?