title: URLにおけるフラグメントと鍵の受け渡し
author: Amane Katagiri
summary: ブラウザでたのしくE2EE
date: 2021-09-20T14:16:00+09:00
category: tech
license: CC BY 4.0
rating: sfw
ai_summary: URLの末尾に付加される「フラグメント（#）」がサーバーに送信されないという性質を利用し、ブラウザ上でエンドツーエンド暗号化（E2EE）を実現する手法を解説した記事です。MEGAなどのクラウドストレージでの活用事例を挙げ、サーバーに鍵を知られずにデータを保護する仕組みを紹介しています。また、使用可能な文字種や文字数の制限、スクリプト注入などのセキュリティ上の注意点についてもまとめられています。
ai_keywords: URLフラグメント, E2EE, エンドツーエンド暗号化, クライアントサイド処理, 鍵の受け渡し, プライバシー保護, MEGA, RFC 3986, セキュリティ対策, 入力検証

[TOC]

/\* この記事は、ライフ 人間と科学シリーズ(Plume)の閉鎖処理の一環として、投稿した情報の整理を行うために書かれた。 \*/

# 概要

インターネット上のリソースを指し示すために使われる **URL** [^url-example]は、スキームや接続先、場所を示す複数の要素で構成されている。このうち、末尾の `#` から始まる要素である **フラグメント** は、一般的なブラウザにおいてサーバに送信されないという特殊な性質を持つ。この記事では、その特徴を利用したアプリケーションの例と、フラグメントを活用する際の注意点について触れる。

[^url-example]: `https://example.com/path/to/resource#fragment` など。

# フラグメントとは

URLは、その上位概念としてのURI - [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html)または[URL Standard](https://url.spec.whatwg.org/)によってその構文が定義されている。URL構文のうちフラグメントはURLの末尾に置かれる `#` から始まる要素で、クライアント側で処理されることを意図した文字列である。[RFC 7230](https://www.rfc-editor.org/rfc/rfc7230.html)ではリクエストにおけるリソースの指定から除外される旨が明記されており、今日の一般的なブラウザでもそのように動作する[^brower-fragment]。

[^brower-fragment]: [ウェブ上のリソースの識別 - HTTP | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web#fragment)

通常のHTMLドキュメントでは、フラグメントは同じドキュメント内のある場所を示しており、ブラウザで開くとその位置まで移動するという挙動を示す。以下の例では「コメントを読む」をクリックすると、アドレスバーに表示されたURLの末尾に `#super_fragment` が付加され、「フラグメントは便利です」に移動する。ブラウザやサイトによっては、瞬時に移動するのではなくスクロールのアニメーションを表示するかもしれない。

```html
<a href="#super_fragment">コメントを読む</a>
<!--
  長大な文書
-->
<div id="super_fragment">フラグメントは便利です</div>
```

SPAなどのHTML上で実装されたアプリケーションでは、JavaScriptによって画面遷移が制御されており、アプリケーションの状態をURLにマッピングするためにフラグメントが使われる場合がある。例えば、 `https://example.com/#!/user/amane/edit` というURLを開くと、 `amane` というユーザの情報を編集する画面が表示されるように設定できる。アプリケーション内のある場所を示すという意味では、通常のHTMLドキュメントと大きな違いはないとも言えるだろう。

さて、このようなフラグメントの使い方は、あくまでURLが示すリソースをさらに絞り込むための副次的な情報としての利用に留まっており、サーバに送信されないという特殊な性質を十分に活かしきれていない。ある情報をクライアントが知りつつサーバが知らないという状況は、エンドツーエンド暗号化（E2EE）とよく似ている。

# E2EEとは

エンドツーエンド暗号化（End-to-End Encryption / E2EE）とは、他者[^isp]を経由したり、他者[^server]が保管・転送するデータ――プライベートなチャットや音声通話・クラウドストレージ上の機密情報などを含むが、それに限らない――を暗号化する際に、データを利用者/送信者/受信者だけが持つ鍵で暗号化する技術である。これにより、通信を傍受しうる必ずしも信頼できない他者が、勝手にその内容を閲覧することを（運用上ではなく）原理的に防いだり、たとえサーバからデータが流出してもその内容を保護することができる。

[^isp]: ここでは契約しているISPや経由する全てのネットワークのこと。
[^server]: ここではメッセンジャーやクラウドストレージといったサービスの提供者のこと。

HTTPSやSMTPSなどのTLSを用いた経路暗号化では、経路上でデータを傍受される心配はなくなるが、依然としてサービスの提供者はそのデータを閲覧することができる。これらのプロトコルはサーバ上のリソースを特定し、利用するためにリクエストを送信するので当たり前の挙動に思えるものの、サーバにリソースの内容を知られたくないケースも多々存在する。リソースの所有者はサービス提供者と必ずしも一致しないからだ。例えば、クラウドストレージに保管するデータは利用者の所有物であり、プライベートなチャットの内容は送信者と受信者のみが知るべき情報である[^bad-rule]。このようなアプリケーションでデータを安全に保つために、E2EEが役立つ。

[^bad-rule]: サービスの規約によってはこの原則が否定されるかもしれない。

クラウドストレージにおけるE2EEについては、単に自分が保管・閲覧する目的のみで使用しており、誰かと共有する必要がないのであれば、手元で自由に暗号化してからアップロードするだけでよい。例えば、[VeraCrypt](https://veracrypt.fr/en/Home.html)などでコンテナを作成して通常のファイルとして同期すれば、管理者からは無意味なバイト列にしか見えないだろう[^no-diff-sync]。

[^no-diff-sync]: もちろん、コンテナ内の一部のファイルのみ同期したりできないので効率は悪い。

一方で、受信者が存在するチャットや音声通話のような状況では、お互いに使用する鍵についての合意が必要不可欠である。素朴にはサーバが鍵の仲介を行うことで実現できるが、E2EEの定義を満たすには、受け渡す鍵をサーバに知られないまま共有するための工夫が必要となる。[Signal](https://signal.org/)で採用されている[二重ラチェットアルゴリズム](https://signal.org/docs/specifications/doubleratchet/)はその実現方法のひとつだが、ここでは深く触れない。

# URLを使った鍵の受け渡し

URLを用いた簡易的なE2EEの実装――リソースに付随する鍵の共有――として、フラグメントを利用することができる。簡易的というのは、この手法が別の暗号化手法や信頼できるチャネルによる通信と組み合わせて使われる補助的な手段であることを示しており、単に手元のデータを送信するだけならそのチャネルで直接送信した方がよい。

抽象的には、以下のような手順でE2EEを実現する。

1. 送信者: データ (D)を自由に暗号化してデータ (E)を作る
1. 送信者→サーバ: データ (E)をサーバにアップロードする
1. サーバ→送信者: データ (E)に紐付くリソースURLを渡す
1. 送信者: 復号に必要な情報をフラグメントとしてURL (R)に付加してURL (S)を作る
1. 送信者→受信者: 信頼できるチャネルを通じてURL (S)を渡す
1. 受信者→サーバ: URL (S)からフラグメントを取り除いてリクエストする[^auto-remove-fragement]
1. サーバ→受信者: URL (R)に紐付くデータ (E)を渡す
1. 受信者: フラグメントの情報を用いてデータ (E)を復号してデータ (D)を得る

[^auto-remove-fragement]: この処理はブラウザが自動で行う。

5を除く処理はサーバが提供するアプリケーション上で行うのが一般的である。多くの場合、JavaScriptを用いて暗号化や復号といったクライアントでの処理を実現している。悪意あるサーバなら、鍵を取り出して不正に送信するような処理を紛れ込ませることは可能だが、少なくともサーバ上で行われる不正とは異なり解析・検知が可能である。

ここからは、実際に簡易的なE2EEとしてフラグメントを用いているアプリケーションをいくつか紹介する。

## フラグメントを通じて鍵を受け渡す例

[MEGA](https://mega.io/)や[Sync](https://www.sync.com/)といったプライバシーを重視するクラウドストレージサービスでは、ファイルの保管にE2EEを用いるのはもちろんのこと、URLでファイルを共有する際にもフラグメントを使用して安全性を保っている。フラグメントより前の部分はサーバ上の場所を示す単なるIDとして機能しており、フラグメントは受け取ったリソースを復号するための鍵としてクライアント側でのみ使用される。

* MEGA: [https://mega.nz/file/P251wDbA#VPWeLJuW0MMsgpI-SunM9EQJtz93r_G1hlGRjOKeV_k](https://mega.nz/file/P251wDbA#VPWeLJuW0MMsgpI-SunM9EQJtz93r_G1hlGRjOKeV_k)
* Sync: [https://ln5.sync.com/dl/87f1a81a0#ikcyuasp-nvf622dk-kqb6zky9-yrk8nkv3](https://ln5.sync.com/dl/87f1a81a0#ikcyuasp-nvf622dk-kqb6zky9-yrk8nkv3)

なお、このテキストファイルはフラグメントを含めた共有URLが公開されているため、もはやE2EEとしては機能していない。

## フラグメントを使わずに鍵を受け渡す例

フラグメントを使わずにパスで鍵を共有しているため、現状の実装では安全性が低いものの、URLを通じて鍵を受け渡すアプリケーションのアイデアとして有用な例を紹介する。

[enzypt](https://enzypt.io/)はEthereumを使って支払いを検証する典型的なDappである。販売者が示したURLからEthereumを支払うと、コンテンツをダウンロードするための情報を受け取ることができる。具体的には、以下のような手順で販売および購入を行う。

1. 販売者: 販売したいコンテンツを暗号化する
1. 販売者→IPFS: 販売したいコンテンツをアップロードする
1. 販売者→enzypt: コンテンツの名前、値段、支払いを受けるEthereumアカウント、IPFSのハッシュと鍵の一部 (A)を送信する
1. enzypt→販売者: コンテンツを指すenzypt上のIDを通知する
1. 販売者: enzypt上のIDと鍵の一部 (B)を付加し、購入用URLを取得する
1. 販売者→購入者: 信頼できるチャネルを通じて購入用URLを送信する
1. 購入者→enzypt: enzypt上のIDを用いて鍵の一部 (A)を含むコンテンツのメタデータを受け取る
1. 購入者→enzypt: 支払いに用いるEthereumアカウントの公開鍵を通知する
1. enzypt→購入者: 公開鍵に紐付けたランダムな文字列を通知する
1. 購入者→enzypt: ランダムな文字列を秘密鍵で署名して送信する
1. enzypt→購入者: トランザクションに付加すべき情報を通知する
3. 購入者→Ethereumネットワーク: 指定された金額を支払う
4. 購入者→enzypt: Ethereumのトランザクションハッシュを通知する
5. enzypt→Ethereumネットワーク: トランザクションを確認して支払いを検証する
6. enzypt→購入者: 支払いが検証できた場合はIPFSのハッシュを通知する
7. 購入者→IPFSノード: 購入したコンテンツをダウンロードする
8. 購入者: 鍵の一部 (A) (B)を用いて復号する

ただし、13は正当な購入者でなくともトランザクションハッシュさえ一致すればコンテンツをダウンロードできてしまうため、URLを公開して販売する用途には向いていない[^no-publish-url]。本来はダウンロードの前に都度8～10のような署名の検証を挟むべきである。

また、購入のたびにトランザクションを発行してチェックする単純な実装なので、ETHの価格変動や手数料の面であまり実用的ではないかもしれない。メタトランザクションの上で適切なトークンを動かした方がよいだろう。

[^no-publish-url]: トランザクションハッシュが一致して、かつURLが公開されていると、購入者でなくともダウンロードして復号できてしまう。支払い済みのトランザクションハッシュは、支払い先のEthereumアカウントが分かれば容易に検索できる。

# フラグメントを活用する際の注意点

フラグメントはサーバに送信されない自由度の高い要素ではあるものの、仕様上または実用上の制限や注意点がいくつか存在する。

## 使用できる文字種

[RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986)や[URL Standard](https://url.spec.whatwg.org/)で定義されている通り、フラグメントは `#` を含むことができない。

> fragment    = *( pchar / "/" / "?" )
> pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"  
> unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"  
> pct-encoded   = "%" HEXDIG HEXDIG  
> sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"  
>               / "*" / "+" / "," / ";" / "="  
> ["""RFC 3986"""](https://datatracker.ietf.org/doc/html/rfc3986)

> A URL-fragment string must be zero or more URL units. 
> The URL units are URL code points and percent-encoded bytes. 
> The URL code points are ASCII alphanumeric, U+0021 (!), U+0024 ($), U+0026 (&), U+0027 ('), U+0028 LEFT PARENTHESIS, U+0029 RIGHT PARENTHESIS, U+002A (*), U+002B (+), U+002C (,), U+002D (-), U+002E (.), U+002F (/), U+003A (:), U+003B (;), U+003D (=), U+003F (?), U+0040 (@), U+005F (_), U+007E (~), and code points in the range U+00A0 to U+10FFFD, inclusive, excluding surrogates and noncharacters. 
> A code point is a Unicode code point and is represented as "U+" followed by four-to-six ASCII upper hex digits, in the range U+0000 to U+10FFFF, inclusive. A code point’s value is its underlying number. 
> A percent-encoded byte is U+0025 (%), followed by two ASCII hex digits. Sequences of percent-encoded bytes, percent-decoded, should not cause UTF-8 decode without BOM or fail to return failure. 
> ["""URL Standard"""](https://url.spec.whatwg.org/)

例えば、[Matrix](https://matrix.org/)で部屋に招待する際に用いられるサービス[matrix.to](https://matrix.to/)では、フラグメントにMatrix上のアドレスを指定してURLをシェアする仕組みになっている。Matrix上のアドレスは、 `!` で始まる内部的なID [`!sAsZJiLgwBDLSpbbPB:matrix.amane.moe`]( https://matrix.to/#/!sAsZJiLgwBDLSpbbPB:matrix.amane.moe) や、 `@` で始まるサーバ上のユーザ名 [`@amane:matrix.amane.moe`](https://matrix.to/#/@amane:matrix.amane.moe) 、 `#` で始まる複数人が参加することを意図した公開アドレス [`#random:matrix.amane.moe`](https://matrix.to/#/%23random:matrix.amane.moe) が存在する。このうち `#` を使用する公開アドレスをシェアする場合は、 `#` を  `%23` とパーセントエンコーディングしなければならない[^use-id-instead-of-address]。

[^use-id-instead-of-address]: 内部的なIDでシェアすることも可能だが、招待ページにそのまま表示されるので視認性が低い。

現状、多くのブラウザではフラグメントに `#` を含んでいても問題なく動作するが、仕様に反するURLであることは知っておくべきだろう。

## 使用できる文字数

[RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616)によれば、URLの最大長は仕様として定められていない。サーバは長すぎるURLを処理できなければ414(Request-URI Too Long)を返すべきと記されているものの、フラグメントはサーバに送信されない文字列のため、単にブラウザの実装（ひいてはマシンのスペック）に依存する。

例えば、Internet Explorer 9以前は2083文字までしか入力できないという制限があったが[^ie-url-max]、今日の多くのブラウザでは仕様上の制限はなく、動作が遅くなったり表示が崩れたりするだけで済むだろう。ただし、URLを受け渡すチャネルによって最大長を制限される場合もあるため、フラグメントを通じて渡す情報をむやみに大きくしない工夫が必要である。

[^ie-url-max]: [Maximum URL length is 2,083 characters in Internet Explorer](https://support.microsoft.com/en-us/topic/maximum-url-length-is-2-083-characters-in-internet-explorer-174e7c8a-6666-f4e0-6fd6-908b53c12246)

## 受け取ったデータの検証

フラグメントを通じて受け取ったデータをJavaScriptで処理する際に、それらを完全に信用するとやっかいな脆弱性に繋がる場合がある。以下の単純な例では、フラグメントにパスを指定して画像を表示することを意図しているものの、任意のスクリプトを注入できてしまう。

```js
// #'onerror='alert(1) でアクセスするとアラートが出る
document.body.innerHTML = "<img src='" + location.hash.substring(1) + "'>";
```

フラグメントを通じて単一の復号キーを渡すのみであれば、英数字や一部の記号のみを使用できるようにすることで多くの攻撃を防ぐことができるだろう。複雑なデータもJSONやXMLによる表現をBase64などでエンコードしてから渡せばよいかもしれないが、デコードして得られた内容についてはやはり検証が必要である。

# まとめ

* URLの末尾を構成するフラグメントは、URL中でサーバに送信されない唯一の要素である。
* フラグメントを適切に用いることで、ブラウザで簡易的なE2EEを実現できる。
* フラグメントを活用する際は、使用できる文字種や文字数に注意して実装すべきである。また、不正なデータの入力に備えて十分に検証してから処理する必要がある。
