暗号化実装リファレンス
実際に動作している暗号化は、記述されている暗号化よりも狭い。その差分こそがこのページの主題である。Railgun が実行するメッセージ暗号化は libsodium の sealed box のみであり — しかもそれが実行されるのは、ネイティブの libsignal モジュールが存在しないクライアント経路に限られる。libsignal が実際に読み込まれる Electron デスクトップビルドでは、ダイレクトメッセージを暗号化するハンドラは依然としてプレースホルダであり、平文を base64 エンコードするだけである。コミュニティおよびチャンネルのメッセージは、いずれの経路でも暗号化されていない。このページの後半で説明する Signal 方式のプロトコルは、記述とレビューが済んでいるだけで、ダウンロードできるどのクライアントにも組み込まれていない。
これはアーキテクチャのリファレンスであり、独立したセキュリティ監査ではない。第 1 節と第 2 節は実際に出荷されているものを説明する。第 4 節から第 7 節は Railgun が目標としている設計を説明するものであり、全体を通して計画中と明示している。ここに書かれた内容を、計画中と記された性質を Railgun クライアントが現時点で満たしているという主張として読んではならない。
現時点で出荷されているもの
| 経路 | 方式 | 状態 |
|---|---|---|
| ダイレクトメッセージ — ブラウザおよび開発ビルド | libsodium の sealed box — X25519 鍵合意、XSalsa20-Poly1305 認証付き暗号化。ネイティブの libsignal モジュールが存在しない場合に選択される。 | 暗号化あり |
| ダイレクトメッセージ — Electron デスクトップビルド | libsignal が読み込まれるため、encryptDm は Electron の IPC ハンドラへ回される — これは平文を base64 エンコードするだけのプレースホルダである。リレーは読み取り可能なテキストを受け取る。 | 暗号化なし |
| コミュニティおよびチャンネルのメッセージ | base64 で包んだ平文として送信される。チャンネル暗号化は記述されているが、平文であることを示すマーカーを返す。 | 暗号化なし |
| X3DH + Double Ratchet | libsignal に対して実装されているが、型としてのみインポートされている。これを実体化する実行時の経路は存在しない。 | 計画中 |
| 秘密鍵の保管 | 鍵は端末上で生成され、端末上に保持される。秘密鍵や識別鍵の素材を送信するコード経路は存在しない。 | 実装済み |
1. sealed box — 実際に動作している暗号化
Railgun がダイレクトメッセージを暗号化する場合、それは libsodium の sealed box — crypto_box_seal 構成 — によって行われる。これは X25519 鍵合意と XSalsa20-Poly1305 認証付き暗号化を組み合わせたものである。実在し、十分にレビューされた暗号技術である。同時に意図的に単純な構成でもあり、その限界は重要である。どのクライアントがこれに到達するかは第 2 節で述べる。ブラウザと開発ビルドは到達し、Electron デスクトップビルドは到達しない。
sealed box の構成方法
// Sender has only the recipient's public key, PK_r
(pk_e, sk_e) = crypto_box_keypair() // fresh, single-use
nonce = BLAKE2b(pk_e || PK_r) // derived, not random
ct = crypto_box(plaintext, nonce, PK_r, sk_e)
sealed = pk_e || ct // ephemeral key is prepended
erase(sk_e)
受信者はメッセージの先頭からエフェメラル公開鍵を取り出し、自身の秘密鍵で同じ共有秘密を再計算して箱を開く。Poly1305 タグにより、改変された暗号文は無意味な平文へ復号されるのではなく、開封に失敗する。
これによって得られるもの
- • リレーおよび通信経路上のあらゆる第三者に対する機密性。
- • 完全性 — 改竄は検出され、黙って復号されることはない。
- • 送信者の匿名性: エフェメラル鍵は誰が送ったかを一切明かさない。
- • セッション確立が不要なため、受信者がオフラインでも機能する。
これによって得られないもの
- • 前方秘匿性はない。受信者の長期秘密鍵は、その受信者宛てに送られた過去すべてのメッセージを開いてしまう。鍵が漏洩すれば、記録された暗号文もろとも漏洩する。
- • 送信者認証はない。sealed box は構成上匿名であり、誰が書いたかを一切証明しない。
- • 侵害後の回復はない。鍵の侵害を乗り越えるためのラチェットが存在しない。
この三つの欠落こそ、X3DH と Double Ratchet が解消するために存在するものであり、だからこそその作業が進行中である。それが実装されるまで、sealed box によるダイレクトメッセージの正直な要約はこうなる。受動的なネットワーク監視者およびリレーに対しては強く、いずれ受信者の端末鍵を入手する敵対者に対しては弱い。そしてこの要約は、このコードが実際に動作している場所にのみ当てはまる。
2. 暗号化されていないもの
コミュニティおよびチャンネルのメッセージ — ほとんどの Railgun 利用者が実際に書き込む内容の大半 — は、現時点でどのクライアント経路でも暗号化されていない。チャンネル暗号化の関数は存在するが、その関数自体が正直である。警告をログに出し、マーカー文字列の後ろに base64 で包んだ平文を返すため、下流の誰もそれを暗号文と取り違えることはない。
// What encryptChannel actually returns
logger.warn("Channel encryption not yet implemented")
ciphertext = base64("[DEVCRYPTO:PLAINTEXT]" + plaintext)
実際上の帰結はこうである。コミュニティおよびチャンネルの内容に関して、Railgun のサーバーはごく普通のチャットサーバーと同じ立場にある。そこに書いた内容をサーバーは読める。グループ鍵配布 — チャンネルごとの鍵を sealed box に入れてメンバーへ渡す仕組み — が計画されている修正であり、まだ出荷されていない。
Electron デスクトップビルドにおけるダイレクトメッセージ
二つめの欠落は、最も多くの人が使っているクライアントにおけるダイレクトメッセージであり、逆に述べてしまいやすいので、コードが実行するとおりの選択ロジックを示す。initCrypto() はメインプロセスに libsignal が読み込まれたかどうかを問い合わせる。@signalapp/libsignal-client は実際の依存関係であり起動時に読み込まれるため、出荷される Electron ビルドでは実際に読み込まれる — そしてその答えによって、sealed box の実装ではなく Electron の IPC 実装が選択される。その encryptDm は crypto:encryptDm ハンドラへ転送され、そのハンドラはプレースホルダである:
// crypto:encryptDm, Electron main process
const plaintextBytes = new TextEncoder().encode(plaintext)
ciphertext: toBase64(plaintextBytes) // not encryption
したがって、第 1 節の sealed box に到達するのは libsignal が存在しない場合 — ブラウザと開発環境である。デスクトップクライアントでは、ダイレクトメッセージは base64 エンコードされた平文としてリレーに届く。そこで libsignal が読み込まれるのは識別鍵とプレキーバンドルを生成するためであり、セッションは一切確立せず、ラチェットも一切行わない — crypto:ensureDmSession は中身のないデバッグログである。
3. Curve25519 — 基盤となる曲線
出荷されている sealed box も、出荷されていない X3DH の設計も、同じ楕円曲線に依拠している。すなわち、高速な Diffie-Hellman 鍵交換のために Daniel J. Bernstein が設計した Curve25519 である。本節は曲線そのものを説明するものであり、両者に当てはまる。
曲線の方程式
y² = x³ + 486662x² + x
over the prime field 𝔽ₚ where p = 2²⁵⁵ − 19
素数 2²⁵⁵ − 19 が選ばれたのは、きわめて高速な剰余演算を可能にするためである。係数 486662 は、必要なセキュリティ特性を備えた安全な曲線を与える最小の値として選ばれた。
鍵生成
1. 秘密鍵: CSPRNG(暗号論的に安全な擬似乱数生成器)を用いて 32 バイトの乱数を生成する。最下位 3 ビットと最上位ビットを 0 にし、上から 2 番目のビットを 1 にすることで鍵をクランプする。これにより鍵は 8 の倍数となり、有効な範囲に収まる。
a = random(32 bytes)
a[0] &= 248 // Clear low 3 bits
a[31] &= 127 // Clear high bit
a[31] |= 64 // Set second-highest bit
2. 公開鍵: 秘密鍵とベースポイント G = 9 とのスカラー倍算を計算する。
A = a · G // Scalar multiplication on Curve25519
安全性は楕円曲線離散対数問題(ECDLP)に依拠する。A と G が与えられても、a を復元することは計算量的に不可能である。Curve25519 は約 128 ビットの安全性を提供する。
Diffie-Hellman 鍵交換
二者(アリスとボブ)は、共有秘密を一度も送信することなくそれを計算できる:
Alice: a (private), A = a·G (public)
Bob: b (private), B = b·G (public)
Alice computes: S = a · B = a · (b·G)
Bob computes: S = b · A = b · (a·G)
Both arrive at S = ab·G — the same shared secret
A と B(いずれも公開)を知る盗聴者は、ECDLP を解かずに S を計算することはできない。これが計算量的 Diffie-Hellman(CDH)仮定である。
sealed box は、この交換の片側をエフェメラルにして使い捨てたものである。以下で述べる X3DH は、この交換を異なる鍵ペアに対して四回行うことで、内容を秘匿するだけでなく双方を認証する結果を得るものである。
4. X3DH — 拡張三重 Diffie-Hellman
計画中X3DH は難しい問題を解決する。一方がオフラインのとき、二人はどうやって暗号化されたセッションを確立するのか。従来の Diffie-Hellman は双方がオンラインであることを必要とする。X3DH は事前にアップロードされた鍵バンドルを用いて、非同期の鍵合意を可能にする。
鍵の種類
識別鍵(IK)
長期の Curve25519 鍵ペア。一度だけ生成され、利用者を暗号学的に識別する。利用者が登録し直さない限り変わらない。
署名付きプレキー(SPK)
中期の Curve25519 鍵ペアで、識別鍵によって署名される。定期的にローテーションされる。署名は、それが識別鍵の保持者に属することを証明する。
ワンタイムプレキー(OPK)
一括してアップロードされる使い捨ての Curve25519 鍵ペア。それぞれ一度だけ使われ、その後削除される。最初のメッセージに対して追加の前方秘匿性を提供する。
エフェメラル鍵(EK)
送信者が新しいセッションごとに生成する新規の Curve25519 鍵ペア。サーバー側には一切保存されない。
X3DH ハンドシェイク
アリスが(オフラインかもしれない)ボブにメッセージを送りたいとき、彼女はサーバーからボブのプレキーバンドルを取得し、四回の Diffie-Hellman 計算を行う:
// Alice has: IKₐ (her identity key), EKₐ (fresh ephemeral)
// Bob published: IKᵦ, SPKᵦ, OPKᵦ
DH1 = DH(IKₐ, SPKᵦ) // her identity ↔ his signed pre-key
DH2 = DH(EKₐ, IKᵦ) // her ephemeral ↔ his identity
DH3 = DH(EKₐ, SPKᵦ) // her ephemeral ↔ his signed pre-key
DH4 = DH(EKₐ, OPKᵦ) // her ephemeral ↔ his one-time pre-key
SK = KDF(DH1 || DH2 || DH3 || DH4)
// SK is the shared secret, derived via HKDF-SHA-256
なぜ DH 演算が四回なのか?
- DH1 は相互認証を提供する(双方の識別鍵が関与する)
- DH2 はエフェメラル鍵がボブの識別に結び付けられることを保証する
- DH3 はエフェメラル鍵によって前方秘匿性を提供する
- DH4 は追加の前方秘匿性を提供する。OPK が利用できない場合でも、X3DH は DH1–DH3 だけで機能する
DH1 は sealed box が持たない演算である。だからこそ、出荷されている経路はメッセージが誰から来たのかを伝えられない。
KDF — 鍵導出関数
生の DH 出力は HKDF-SHA-256(HMAC ベースの鍵導出関数)を用いて結合される。HKDF は連結された DH 出力からエントロピーを抽出し、それを一様ランダムな共有秘密へと拡張する:
PRK = HKDF-Extract(salt="", input=DH1||DH2||DH3||DH4)
SK = HKDF-Expand(PRK, info="RailGunX3DH", length=32)
5. Double Ratchet アルゴリズム
計画中X3DH が最初の共有秘密を確立すると、そこからは Double Ratchet が引き継ぐ。かみ合った二つの「ラチェット」を用いて、メッセージごとに新しい固有の鍵を導出する。この設計は過去のメッセージを保護し、新たなラチェット段階の後に将来のメッセージの保護を回復することを意図している。
DH ラチェット(非対称)
会話の方向が変わるたびに(アリス → ボブ、次にボブ → アリス)、新しいエフェメラル鍵を用いて新たな Diffie-Hellman 鍵交換が行われる。これによりルート鍵が前方へ「ラチェット」される:
dh_out = DH(my_ratchet_key, their_ratchet_key)
root_key, chain_key = KDF(root_key, dh_out)
これが侵害後セキュリティを提供するものである。現在の状態を盗んだ攻撃者は、新たな DH ラチェット段階が発生した時点でアクセスを失う。
対称ラチェット(ハッシュチェーン)
DH ラチェットの段階の間では、ハッシュベースのラチェットがチェーン鍵からメッセージごとに新しいメッセージ鍵を導出する:
message_key = HMAC-SHA256(chain_key, 0x01)
chain_key = HMAC-SHA256(chain_key, 0x02)
古いチェーン鍵は各段階の後に削除される。メッセージ鍵はちょうど一通のメッセージを暗号化し、その後削除される。これが前方秘匿性の仕組みである。
ラチェットの進行
図示すると、Double Ratchet は次のようになる。各矢印は一方向関数であり、前へは進めるが後ろへは決して戻れない:
Root Key Chain:
RK₀ ──DH──→ RK₁ ──DH──→ RK₂ ──DH──→ RK₃ ...
Each RK step produces a Chain Key:
↓ ↓ ↓
CK₀ CK₁ CK₂
↓ ↓ ↓
Each CK step produces Message Keys:
CK₀→MK₀ CK₁→MK₃ CK₂→MK₅
CK₀→MK₁ CK₁→MK₄ CK₂→MK₆
CK₀→MK₂ CK₂→MK₇
各 MKₙ はちょうど一通のメッセージを暗号化してから削除されることになるため、MK₄ を入手した攻撃者も MK₃ や MK₅ を導出することはできない。現時点で出荷されている sealed box の経路には、盗むべきメッセージごとの鍵も、たどるべきチェーンも存在しない。そこにあるのは、すべてを開いてしまう一つの長期鍵だけである。
6. 前方秘匿性 — 実現に必要なもの
計画中前方秘匿性とは、長期鍵が侵害されても過去のセッション鍵は侵害されないことを意味する。Railgun は現時点でそれを備えていない。それを提供しうる仕組みは二つあり、いずれも上で述べた計画中のプロトコルに属する:
X3DH におけるエフェメラル鍵
送信者は新しいセッションごとに新規のエフェメラル鍵ペアを生成し、ハンドシェイクの後にその秘密側を削除する。後になって双方の識別鍵を盗んだ攻撃者でも、セッションの秘密を再構成することはできない。sealed box も送信者側では確かに新規のエフェメラル鍵を使う — しかし交換の受信者側は長期鍵であり、まさにそれゆえにこの性質は成り立たない。
ラチェット鍵の削除
Double Ratchet は古いチェーン鍵とメッセージ鍵を使用後に削除する。KDF チェーンは一方向である — CKₙ が与えられれば CKₙ₊₁ は計算できるが CKₙ₋₁ は計算できない。したがって時点 T における侵害は、T より前に送られたものを一切明かさない。
侵害後セキュリティ
DH ラチェットは将来秘匿性ももたらす。現在のセッション状態を侵害した攻撃者は、次のラチェット段階でそれを失う。その段階では攻撃者の持たないランダム性が投入されるからである。ラチェットがなければ、侵害された鍵は攻撃者にとって無期限に有用なままである。
7. リレーとキューのアーキテクチャ
Railgun はエンベロープを中継し、オフライン配信のために一時的にキューへ入れることがある。リレーはメッセージの平文を必要としないように作られているが、現時点では二つの場合に平文を受け取っている。どのクライアントも暗号化しないチャンネルメッセージと、IPC ハンドラが暗号化ではなく base64 エンコードを行う Electron デスクトップビルドからのダイレクトメッセージである。sealed box の経路からのダイレクトメッセージは、リレーが開けない暗号文として到着する。
サーバーから見えるもの
サーバーが保持するもの:
- • 公開鍵バンドル
- • 利用者の登録情報
- • コミュニティ/チャンネルのメタデータと所属情報
- • 開くことのできない sealed box の暗号文(ブラウザ/開発経路からの DM)
- • Electron デスクトップビルドからの base64 エンコードされた DM の平文
- • チャンネル暗号化が出荷されるまでの間、チャンネルメッセージの内容
- • 認証の監査記録
サーバーが決して受け取らないもの:
- • 秘密鍵および識別鍵
- • クライアントのセッション状態
- • メッセージ履歴の永続的な複製
メッセージはオンラインの受信者へ WebSocket 経由でリアルタイムに中継される。オフラインの端末向けには、エンベロープが最大 30 日の TTL で Redis にキューされ、端末が受信を確認した時点で削除される。サーバー側のメッセージアーカイブは存在しない — クライアントの履歴は自分の端末のローカルストレージに置かれるため、端末を失うことは履歴を失うことである。
8. セキュリティ特性のまとめ
| 特性 | 方式 | 状態 |
|---|---|---|
| DM の機密性 | sealed box はブラウザ/開発経路でのみ。Electron デスクトップのハンドラは base64 エンコードを行う | 部分的 |
| DM の完全性 | sealed box の経路では Poly1305 タグ。Electron デスクトップの経路は認証のない base64 を送る | 部分的 |
| 鍵が端末に留まること | ローカルの鍵ストア。秘密鍵を送信するコード経路は存在しない | 実装済み |
| オフライン配信 | エンベロープは 30 日の TTL で Redis にキューされ、受信確認時に削除される | 実装済み |
| チャンネルの機密性 | グループ鍵配布は未実装。内容は読み取り可能な形で送信される | 未実装 |
| 送信者認証 | sealed box は構成上匿名である | 未実装 |
| 前方秘匿性 | Double Ratchet を必要とするが、どのクライアントもそれを実体化しない | 計画中 |
| 侵害後の回復 | DH ラチェットを必要とするが、どのクライアントもそれを実体化しない | 計画中 |
| 独立監査 | 完了した第三者による暗号監査は一切主張していない | 未完了 |
| ポスト量子保証 | 包括的なポスト量子セキュリティの保証は一切主張していない | 主張なし |
このページの出所
上記のすべての状態は、設計文書ではなくクライアントのソースコードから読み取ったものである。クライアントのリポジトリは現時点で公開されていないため、読者自身に確認してもらうことはできない — それゆえ、このページは証明ではなく開示として重み付けするのが妥当である。特定の記述についての質問は security@railgun.chat まで歓迎する。