暗号化ファイル共有アプリをゼロから構築する
Step-by-step tutorial to build an encrypted ファイル共有 application. Frontend 暗号化, セキュア backend, and deployment walkthrough.
暗号化ファイル共有アプリを構築するとは、暗号処理をサーバーでなくブラウザに置くことを意味する。個人情報保護法(APPI)が定めるデータ保護原則に最も忠実な設計だ。最小スタックはこうなる:Web Crypto APIによるAES-256-GCM暗号化を行うVite+ReactフロントエンドとFastifyバックエンド、S3互換ストレージ(Cloudflare R2またはBackblaze B2)、そして復号鍵が#以降に収まるため絶対にサーバーに届かない短い共有URLだ。約400行のコードで動作バージョンを出荷でき、月5ドル以下でホストできる。
実際に重要なアーキテクチャの決断
最も重要な決断は暗号鍵をどこに置くかだ。鍵が一度でもサーバーに触れれば、エンドツーエンド暗号化ではなくサーバーサイド暗号化に余分な手順を加えただけになる。正しいパターン:ブラウザでランダムな256ビット鍵を生成し、ファイルを暗号化し、暗号文をアップロードし、URLフラグメントに鍵を入れる(https://yourapp.com/f/abc123#k=base64key)。ブラウザはHTTPリクエストでフラグメントを送信しないため、鍵はクライアントサイドに留まる。
二番目の判断:チャンク分割アップロード。100MB以上のファイルには再開可能なマルチパートアップロードが必要で、そうしないとフラキーなWi-Fiが転送を台無しにする。S3のマルチパートAPIは5MB最小チャンクと最大10,000パートをサポートし、50GBの上限を提供する。最初から計画しておくこと。
プロジェクトの骨格セットアップ
2つのパッケージから始める:Vite+Reactフロントエンドと Fastifyバックエンド。フロントエンドがすべての暗号処理を担い、バックエンドがストレージとメタデータを処理する。TypeScriptを使うことでArrayBufferとCryptoKeyの型安全性が得られる。
バックエンドの依存関係に追加するもの:@fastify/multipart、@aws-sdk/client-s3、@aws-sdk/s3-request-presigner、共有メタデータ用のbetter-sqlite3。SQLiteスキーマは最小限に:shares(id, object_key, size_bytes, expires_at, download_count, max_downloads)。ファイル名なし、ユーザーデータなし、IPなし。
フロントエンド暗号化パイプラインの実装
鍵を生成し、AES-256-GCMで暗号化し、URLフラグメント用のbase64鍵と共に暗号文Blobを生成する:
async function encryptFile(file: File) {
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const plaintext = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, key, plaintext
);
const rawKey = await crypto.subtle.exportKey('raw', key);
const blob = new Blob([iv, new Uint8Array(ciphertext)]);
return { blob, keyBase64: toBase64(rawKey) };
}
50MB以上のファイルは4MBチャンクを暗号化してReadableStreamに追加するストリーミング版に差し替える。モバイルSafariのヒープは約400MBの単一ArrayBufferで詰まる。
アップロードエンドポイントの設計
バックエンドは有用な情報を知るべきでない。暗号文のPOSTを受け付け、ランダムな16文字のURL安全IDを生成し、SQLiteに有効期限付きで保存し、本文をS3に直接ストリームする:
fastify.post('/upload', async (req, reply) => {
const id = nanoid(16);
const key = `blobs/${id}`;
const upload = new Upload({
client: s3,
params: { Bucket: 'hexa-transfers', Key: key, Body: req.raw }
});
await upload.done();
db.prepare('INSERT INTO shares VALUES (?, ?, ?, ?, 0, ?)').run(
id, key, req.headers['content-length'], Date.now() + 7*86400*1000, 10
);
return { id };
});
デフォルト有効期限7日、最大10ダウンロード。積極的なデフォルトを設定しないと無制限のストレージ肥大化が起きる。有効期限切れのBlobを毎晩削除するcronを使うこと。
フラグメント鍵付き共有リンクの生成
アップロード完了後、クライアントサイドで共有URLを構築する:
const { id } = await uploadResponse.json();
const shareUrl = `${location.origin}/f/${id}#k=${keyBase64}`;
このフラグメントはユーザーのブラウザを離れない。受信者がリンクをクリックすると、Reactアプリがwindow.location.hashを読み取り、鍵を解析し、暗号文をフェッチし、ローカルで復号する。サーバーは悪意あるJavaScriptをユーザーにプッシュする以外に平文へのパスを持たない—だからこそ厳格なContent Security Policyとサブリソースハッシュのピンニングをデプロイすべきだ。
GDPR/APPIフレンドリーな設計
サーバーが暗号文しか見ないため、GDPRの4条定義に該当する個人データがほとんど存在しない。ただし、プロセッサー関係(R2とDigitalOcean)を文書化し、30日のBlob保持上限を設定し、共有リンクに鍵が含まれるため送信者が信頼するチャネルで送信すべきという明確な通知を公表すること。IPあたり時間50件のアップロードレート制限でユーザーIDをログに記録せずに悪用を抑制できる。
HexaTransferはこのアーキテクチャ上に構築されている—ブラウザでのWeb Crypto、不透明なBlobストレージ、URLフラグメント内の鍵、サーバーに平文ゼロ。詳細は https://hexatransfer.com で。無料、アカウント不要、最大10GB。
MVPが動いた後に追加すること
基本フローが出荷されたら、最も価値の高い追加機能は:フラグメント鍵の上へのパスワード保護(PBKDF2、600,000回反復)、使い捨てメールボックスによるダウンロードごとのメール通知、内容を明かさずに暗号文ハッシュに基づく悪用報告。アカウントは特定の理由がない限り追加しないこと—アプリをプライバシーツールからデータ負債に変えてしまう。小さく出荷し、意見を持ち続け、暗号処理に重作業をさせること。
エンドツーエンド暗号化で大容量ファイルを安全に送信
エンドツーエンド暗号化で最大10GBのファイルを無料で転送。アカウント不要。ファイルはアップロード前にブラウザで暗号化されるため、他の誰にも読まれません。
ファイルを送信