🔥

ゼロ知識アーキテクチャで自己破壊型ノート/チャットをCloudflare WorkersとDurable Objectsで作った

に公開

はじめに

サーバーが平文を一切見ない自己破壊型ノート/チャットサービス Flashpaper をCloudflare Workers + Durable Objectsで作りました。

https://flashpaper.ravers.workers.dev

Flashpaper とは

Flashpaperは、ブラウザ内でAES-256-GCM暗号化を行い、暗号鍵をURLフラグメントに埋め込むことで、サーバーが平文を見ることが技術的に不可能な設計のPrivnote代替サービスです。

特徴

  • 🔐 ゼロ知識: サーバーは暗号文しか見ない
  • 📝 自己破壊型ノート: 一度読んだら暗号文ごと消滅
  • 💬 自己破壊型チャット: 常に1メッセージだけ存在、返信で前のメッセージは完全削除
  • 🌍 セルフホスト可能: Cloudflare無料枠で動作
  • 🔓 オープンソース: コードを監査可能

なぜ作ったのか

既存ソリューションの課題

サービス 課題
Privnote サーバー側で平文を見れる可能性。運営を信用する必要がある
手動で暗号化 鍵の共有が面倒。利便性が低い
Signal等 アプリのインストールが必要。ワンショット共有には過剰

「運営を信用しなくていいメモ共有サービスが欲しい」

この願望からFlashpaperは生まれました。

アーキテクチャ

URLフラグメントの仕組み

HTTPの仕様上、URLの#以降はサーバーに送信されません。

https://flashpaper.example.com/view/abc123#ENCRYPTION_KEY_HERE
                                           ↑
                                    サーバーには届かない

この仕組みを活用して、暗号鍵をURL内に埋め込んでいます。

完全な自己破壊

Flashpaperでは、メモを閲覧した瞬間に暗号文自体もデータベースから完全に削除されます。

// メモ取得時の処理
const note = rows[0];

// 復号に必要な暗号文を返却する前に、データベースから完全削除
this.sql.exec(`DELETE FROM notes WHERE id = ?`, note.id);

return Response.json({ success: true, ciphertext: note.ciphertext });

「読んだら消える」は暗号文レベルで実現されており、サーバー上には何も残りません。

技術スタック

技術 用途
Cloudflare Workers エッジでのリクエスト処理
Durable Objects + SQLite (一時)データ永続化
Web Crypto API AES-256-GCM暗号化
Cron Triggers 期限切れデータの自動削除

技術的なこだわり

Durable Objectsを選んだ理由

  • 🚀 エッジで完結: コールドスタートが高速
  • 💾 SQLite内蔵: 無料枠でも使える
  • 🔒 分離性: 各メモが独立したインスタンス
  • ⏰ Cron対応: 定期クリーンアップが簡単
export class NoteStore {
  constructor(state, env) {
    this.state = state;
    this.sql = state.storage.sql;
    
    this.state.blockConcurrencyWhile(async () => {
      this.sql.exec(`
        CREATE TABLE IF NOT EXISTS notes (
          id TEXT PRIMARY KEY,
          ciphertext TEXT NOT NULL,
          created_at INTEGER NOT NULL,
          expires_at INTEGER
        )
      `);
    });
  }
}

発展: Cloudflare有料プランでの完全揮発実装

現在の実装ではSQLiteを使用していますが、Cloudflareの有料プランを利用すればDurable ObjectsのTransient Storage(揮発性メモリ) を活用した完全インメモリ実装も可能です。

// 有料プラン向け: 揮発性ストレージを使った実装例
export class VolatileNoteStore {
  constructor(state, env) {
    this.state = state;
    // SQLiteの代わりにインメモリMapを使用
    this.notes = new Map();
  }

  async fetch(request) {
    // Durable Objectインスタンスが破棄されると
    // メモリ上のデータも完全に消滅
  }
}

この実装では、Durable Objectインスタンスがアイドル状態で一定時間経過すると自動的に破棄され、メモリ上のデータも完全に消滅します。ディスクに一切書き込まれないため、より強力なプライバシー保証が得られます。

Web Crypto APIによる暗号化

外部ライブラリに依存せず、ブラウザネイティブのAPIで暗号化を実装しています。

async function encrypt(plaintext, key) {
    const encoder = new TextEncoder();
    const data = encoder.encode(plaintext);
    const iv = crypto.getRandomValues(new Uint8Array(12));
    
    const ciphertext = await crypto.subtle.encrypt(
        { name: 'AES-GCM', iv }, 
        key, 
        data
    );
    
    const combined = new Uint8Array(iv.length + ciphertext.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(ciphertext), iv.length);
    return btoa(String.fromCharCode(...combined));
}

チャット機能のセッションバインディング

チャットでは、最初にURLを開いたブラウザにセッションを紐づけることで、URL漏洩時のリスクを軽減しています。

// 初回アクセス時にセッションを紐づけ
if (!chat.creator_session_hash) {
    this.sql.exec(
        `UPDATE chats SET creator_session_hash = ? WHERE id = ?`, 
        sessionHash, chat.id
    );
}

// 別ブラウザからのアクセスは拒否
if (chat.creator_session_hash !== sessionHash) {
    return Response.json({ 
        success: false, 
        error: 'Session mismatch' 
    }, { status: 403 });
}

実装で踏んだ地雷

Twitterの埋め込みブラウザ対応

Twitter内ブラウザではconfirm()がブロックされるため、カスタムモーダルを実装しました。

// ❌ Twitterブラウザで動かない
if (confirm('削除しますか?')) { ... }

// ✅ カスタムモーダルで対応
function showModal() {
    document.getElementById('modal-overlay').classList.add('show');
}

Durable Objectsのスキーママイグレーション

既存インスタンスとの互換性を保つため、防御的なマイグレーションが必要です。

this.state.blockConcurrencyWhile(async () => {
    const tableInfo = this.sql.exec(`PRAGMA table_info(chats)`).toArray();
    const hasColumn = tableInfo.some(col => col.name === 'creator_session_hash');
    
    if (!hasColumn) {
        this.sql.exec(`ALTER TABLE chats ADD COLUMN creator_session_hash TEXT`);
    }
});

プライバシーモデル

アプリケーションが保存するもの(一時的)

データ 保存場所 セキュリティ 削除タイミング
暗号文 Durable Objects 鍵なしで復号不可 閲覧時に即座に削除
トークンハッシュ Durable Objects SHA-256でハッシュ化 チャット破棄時
セッションハッシュ Durable Objects SHA-256でハッシュ化 チャット破棄時

アプリケーションが保存しないもの

  • ❌ 平文(そもそもサーバーに届かない)
  • ❌ 暗号鍵(URLフラグメントのみ)
  • ❌ 元のトークン/セッションID

注意点

Cloudflareのインフラレベルでは、IPアドレスやリクエストメタデータがログに残る可能性があります。ただし、メッセージ内容は暗号化されているため、サーバーアクセス権があっても復号できません。

使い方

セルフホスト

# Clone
git clone https://github.com/M-Igashi/flashpaper.git
cd flashpaper

# Install
npm install

# Deploy
npx wrangler deploy

これだけで、あなた専用のFlashpaperが立ち上がります。

まとめ

Flashpaperは、「信用してください」と言わなくていい設計を目指して作りました。

  • サーバーは平文を見れない
  • 暗号文も閲覧後に完全削除
  • オープンソースでコードを監査可能
  • セルフホストで運営者を排除可能

「検証可能な技術」でプライバシーを実現する。それがゼロ知識アーキテクチャの本質です。

https://github.com/M-Igashi/flashpaper

⭐ スターをいただけると励みになります!

Discussion