title: 文フリ34のふりかえり（ダウンロードシステム編）
author: Amane Katagiri
summary: たのしい文化祭ダウンロードシステムまとめ
date: 2022-06-12T19:00:00+09:00
modified: 2022-06-13T01:39:00+09:00
category: ugoki
license: CC BY 4.0
rating: sfw
ai_summary: 第三十四回文学フリマ東京で頒布された「光速感情デラックス」の電子版配布システムに関する技術解説記事です。一般的なメディア配布やダウンロードリンク方式の制約を整理しつつ、独自のセキュリティ要件を満たすためにCloudflare Workers、KV、R2を用いて構築したシステム「Quetta」を紹介。bcryptによるパスワード検証やタイミング攻撃対策など、安全な頒布を実現するための実装手法をまとめています。
ai_keywords: 文学フリマ, 電子書籍, ダウンロードリンク方式, Cloudflare Workers, Cloudflare KV, Cloudflare R2, セキュリティ, bcrypt, 光速感情デラックス, 同人誌頒布

[TOC]

# 概要

[第三十四回文学フリマ東京](https://bunfree.net/event/tokyo34/)の新刊「[光速感情デラックス](https://bunfree34.hentaigirls.net/)」は、配布用のポストカードに記載されたQRコードからPDF版をダウンロードできるシステム（ダウンロードリンク方式）を採用しました。実はEPUB版を頒布する計画も出ているのですが、まだ必要な作業が残っています。

今回の配布方法を実装するには、いくつかの制約と特殊な条件を解決する必要がありました。この記事では、光速感情デラックスのダウンロードシステムの要件について、一般的なダウンロードリンク方式と比較しながら整理した上で、それらを[Cloudflare Workers](https://developers.cloudflare.com/workers/)+[KV](https://developers.cloudflare.com/workers/learning/how-kv-works/)+[R2](https://developers.cloudflare.com/r2/)上で実装する流れについて紹介しようと思います。

# 電子版の配布方法について

## メディア配布方式のこと

私たちが電子版[^ebook]を配布するとき、コンテンツが紙に記録された状態で頒布される *紙の本* の特徴をできるだけ維持する方法が **メディア配布方式** です。これは、BD-R(OM)やCD-R(OM)、(micro/mini)SDカードやUSBフラッシュドライブ、フロッピーディスクなどの持ち運びしやすいメディアにコンテンツを記録して頒布する方法です。

[^ebook]: PDFファイルやテキストファイル、EPUBファイルはもちろん、複数のPNGファイルやJPGファイルをZIPファイルに固めたものを配布することさえできます。

即売会では、印刷版の場合と同様にコンテンツを記録したメディアを渡せば頒布が完了します。[変態美少女ふぃろそふぃ。](https://hentaigirls.net/)では、[第二十八回文学フリマ東京](https://bunfree.net/event/tokyo28/)にて[フロッピーディスクを頒布した回](https://twitter.com/hentaigirlsnet/status/1125232976097210369)がありました[^floppy]。

![ボールチェーンが付いた黒色・オレンジ色・ピンク色のフロッピーディスクで配布された「flowline flower」](/images/bunfree34/floppy.jpg){width=750 height=600}

[![「flowline flower」フロッピーディスク版に貼付されていたラベル](/images/bunfree34/flr-label_thumb.png){width=600 height=600}](/images/bunfree34/flr-label.png)

"""フロッピーディスクのラベルデザインは[ジェンガ2003](https://twitter.com/NTSC_J)と片桐天音によって[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)でライセンスされています。"""

[^floppy]: 当時はいろいろ切羽詰まっていて、ちゃんとメディアそのものの写真を撮る暇がなかったみたいです。[隣のサークルさんが上げてくれた写真](https://twitter.com/amecokaeruda/status/1125270163295596544)などではっきりとした姿を確認できます。

メディア配布方式は、印刷版が持っている「その場で」そして「オフラインで」コンテンツを楽しめるという特徴を引き継いでいます。これは、以降に述べる他の方式と比較すると、頒布時点でメディアに全てのコンテンツが記録されているという点で非常に優秀です。全てのデータを読者自身で管理することで、読者は *信頼できないサークル* からも購入できるようになりますし、サークル側もダウンロードシステムの維持・管理から解放されるという大きなメリットを享受できます。

しかし、メディアそのものがコンテンツを表示するインターフェースを備えていない限り[^epaper]、ファイルを読み込んで内容を表示できるデバイスと、そのデバイスにメディアを読み込む手間が必要です。USBポートやメモリーカードリーダーを備えたノートPCを使っていればあまり負担を感じないかもしれませんが、スマートフォンで気軽にコンテンツを利用したい場合はかなり不便でしょう。

[^epaper]: たとえば、数GB程度の記憶領域を持つ電子ペーパーが数百円で入手できるようになれば実現できます。

そのため、最近ではメディア配布方式の利用シーンは大容量の画像集や法的懸念があるコンテンツなどに限られ、次のダウンロードリンク方式が利用されることが多くなっています。

## ダウンロードリンク方式のこと

私たちが電子版を配布するとき、インターネットが広く普及している状況を最大限に利用してコンテンツをダウンロードしてもらう方法が **ダウンロードリンク方式** です。これは、コンテンツの場所を示すURLやダウンロードに必要な秘密情報[^downloadurl]を記載したメディア（ポストカードやパンフレットなど）を頒布し、購入した読者自身でコンテンツをダウンロードしてもらう方式です。

[^downloadurl]: URLと秘密情報は分離されていることもありますし、URL内に（手で入力するには複雑で長い）秘密情報を含んだ状態でQRコードに記録されていることもあります。

ここでいうメディアは、印刷物としての紙やプラスチックシートなどはもちろん、メディア配布方式で一般的に利用されるSDカードなども含んでいます[^wastesdcard]。ダウンロードリンク方式の最も重要な特徴はインターネットを利用してダウンロードできることであり、メディアそのものには完全なコンテンツが記録されていないという点で、メディア配布方式とは大きく異なります。

[^wastesdcard]: もちろん、数GB～数百GBを記録できるメディアにURLやパスワードだけを記録するのは無駄なので、通常は利用されません。

もちろん、URLや秘密情報も人間の目が識別できる形式で印刷されている必要はなく、URLをQRコードとして記載したり、NFCタグを読み取ってリンクを開いてもらうようにすることもできます。QRコードやNFCタグを読み取る機能がないデバイスを使っている場合は、別のデバイスで読み取った上で転送するとよいでしょう。わずかに不便かもしれませんが、コンテンツそのものを送信するより簡単で省エネです。

新刊「[光速感情デラックス](https://bunfree34.hentaigirls.net/)」や準新刊「[先輩、今日もいいですか](https://hentaigirls.net/post/tokyo33/#_1)」でも、ダウンロードリンク方式を採用しています。宛名面の左下に記載されているQRコードがダウンロードリンクです。

![光速感情デラックス ポストカード（左側は裏面で、右側は表面）](/images/bunfree34/faster-emotion-deluxe_postcard.png){width=1014 height=750}

"""通信面および宛名面のデザインは[ごまふわラビ](https://twitter.com/gomafu_warabi)によって[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)でライセンスされています。"""

ダウンロードリンク方式では、受け取ったメディアそのものにはコンテンツが記録されていないので、読者は一時的な障害や恒久的なリンク切れなどに備えて確実にファイルを入手しておく必要があります。メディア配布方式は印刷版の電子化にあたる方式ですが、ダウンロードリンク方式はむしろ印刷版との引換券のような性質を持っているからです。

なお、ファイルを手元にダウンロードしたあとは、外形的にはメディア配布方式とほとんど差がありません。強いてメディア配布方式と異なる点を挙げるなら、入手したファイルを紛失しても再度ダウンロードできるかもしれない点、記録するメディアは読者自身で入手しなければならない点があるでしょう。

また、サークル側はダウンロードシステムを確実に維持・管理する必要があり、場合によっては配布から一定期間でシステムを廃止する可能性がある旨を明記する必要があります。この点に注目すると、メディア配布方式は読者に負担を強いる方式、ダウンロードリンク方式はサークル側に負担を強いる方式ということができるかもしれません。

## 電子書籍ストアのこと

一般的なサークルが即売会で利用するには少しハードルが高いですが、電子書籍ストアやコンテンツ販売サイトが作品を無料配布できるクーポンを発行できる機能を持っていれば、そのクーポンを記載したメディアを頒布する形態を利用できます。

たとえば、[BOOTH](https://booth.pm/)の[シークレット公開](https://booth.pm/booth_apps/secret_pass)を利用すると、URLと合言葉（パスワード）を知っている人だけに商品を公開できます。このURLとパスワードを頒布することで、サークル側はダウンロードシステムの維持・管理を避けつつ簡単にコンテンツをダウンロードしてもらえるでしょう。

また、特定の作品にのみ利用できるギフト券を購入できるストアであれば、そのクーポンコードを使って頒布できるかもしれません。ただし、本来の目的とはかなり離れた使用方法なので、不便な部分があるのはもちろん、ストアによっては規約違反となる可能性があります。たとえば、同じ作品のギフト券について購入回数を制限していたり、有効期限が180日間～1年間と頒布に使うには短かったり、そもそもクーポンの転売を禁止している場合さえあります。

なお、ダウンロードシステムとして特定のストアを利用した場合、そのサービスを利用できない読者を実質的に排除することになったり、ストアの閉鎖によってコンテンツが配布できなくなってしまう点には注意すべきでしょう。

# 光速感情デラックスのダウンロードシステムについて

## 制約と条件のこと

[文フリ34のふりかえり（ポストカード編）](/post/bunfree34-postcard/)では、光速感情デラックスの配布メディアの詳細についてお伝えしました。このポストカードの形式について検討すると、配布方法を決めるにあたって重要な以下の特徴を持っています。

* URLが **推測可能** で、秘密情報を含んでいないこと
* メディア全体で共通の秘密情報を持って **いない** こと
* メディアごとに異なる秘密情報を持って **いる** こと

一方で、一般的なダウンロードリンク方式における配布メディアは、以下のような特徴を持っています。

* URLの **推測が難しく** 、実質的に秘密情報を含んでいること
* メディア全体で共通の秘密情報を持って **いる** こと
* メディアごとに異なる秘密情報を持って **いない** こと

一般的なダウンロードリンク方式における配布メディアは、費用を抑えるために共通の内容で印刷することが多く、その場合はURL自体を秘匿したり共通の秘密情報を配布するのが自然です。たとえば、 `https://example.com/pthc-book/BvTR3rPt` というURLをQRコードで記載し、秘密情報として `16574894` という文字列を人間の目が識別できる形式で記載すれば、全て同じ内容のポストカードのうち1枚を引き渡すだけで頒布が成立します。

このメディアに対応するダウンロードシステムでは、  `https://example.com/pthc-book/BvTR3rPt` というURLに対してパスワード入力画面を表示し、 `16574894` という入力があればコンテンツを返すように実装すればよいでしょう。場合によっては、 `https://example.com/pthc-book/cWus4SqU` という間違ったURLに対しても同じ画面を表示したり、パスワードの検証に時間をかけたりする[^heavyverify]ことで、購入者以外がコンテンツを閲覧できないよう工夫する必要があるかもしれません。

[^heavyverify]: システムがわざとレスポンスを遅くするよりも、[Argon2](https://ja.wikipedia.org/wiki/Argon2)などのリソース消費が大きくなるように設計されたアルゴリズムを使うべきです。

しかし、光速感情デラックスのポストカードは、一般的なダウンロードリンク方式における配布メディアの性質を備えていません。URLは[文フリ34のふりかえり](/post/bunfree34-general/)でも示しているとおり [`https://hentaigirls.net/book/faster-emotion-deluxe/`](https://hentaigirls.net/book/faster-emotion-deluxe/) であり、作品タイトルや[告知記事](https://hentaigirls.net/post/tokyo34/)から十分に推測できる文字列です。また、ポストカード自身には秘密情報が一切記載されていません。これは、無人販売企画の進行にあたって、ブースに置かれたポストカードを不正に持ち去ってもコンテンツを閲覧できないようにするための工夫です。

唯一、秘密情報として利用できるのが印刷版チケットです。そのため、印刷版チケットに記載された変更用トークンを、PDF版のダウンロードパスワードとしても利用できるようにする必要があります。印刷版チケットは購入者が受け取るポストカードにのみ貼付されているものであり、他の人は閲覧できません。当初はここにダウンロード用の共通パスワードを記載する計画がありましたが、利用者が変更用トークンと区別できない可能性が高く、またシールの大きさ（約18mm×約20mm）も小さかったので断念しました。

![光速感情デラックス 印刷版チケット](/images/bunfree34/ticket.jpg){width=300 height=350}

この要件は、ポストカードの一部を空白にした状態で印刷し、後から回転印などでシリアルナンバーを打刻するような方式でも同様です。これらの頒布方法に対応するダウンロードシステムでは、1つのURLに対して複数の秘密情報を利用できるように設計しなければなりません。

## 実装のこと（共通パスワードの場合）

共通のパスワードを1つだけ利用できるようにする場合は、幅広いダウンロードシステムを利用できます。

たとえば、[ギガファイル便](https://gigafile.nu/support.php)などのファイル転送サービスではダウンロードパスワードを設定できるものが多いですし、[Sync](https://www.sync.com/)などのクラウドストレージでも[パスワードつき共有リンク](https://www.sync.com/help/how-to-manage-shared-link-permissions/)を発行できる場合があります。非常に簡易な形式なら、パスワードを設定したZIPファイルを配布するだけでも実現できます。

しかし、これらの方式で複数のパスワードを利用可能にするのは困難です。100個のパスワードを1つずつ設定した100個のファイルをアップロードしても、購入者は自分の持っているパスワードがどのファイルに対応しているか判断できません。パスワードとは別にIDを発行したり、十分に長いパスワードの先頭から数桁をファイル名に含めれば対応できる可能性がありますが、利便性の部分で難が残ります。

## 実装のこと（リライトルールの場合）

ここで、[あまねけ！](/)や[変態美少女ふぃろそふぃ。公式サイト](https://hentaigirls.net/)のように、リライト[^rewrites]ルール（またはリダイレクト[^redirects]ルール）を記述できるホスティングサービスを利用する場合を考えます。

[^rewrites]: ここでは、あるURLに対して（URLを変更せずに）別のURLのコンテンツを返すことを示しています。
[^redirects]: ここでは、あるURLに対して別のURLに転送することを示しています。

リライトルールを使用すると、複数のパスワードに対応した簡易的なダウンロードシステムを実装できます。つまり、ダウンロードパスワードを含むURLを推測不可能なURLにリライトして配布を実現します。具体的な手段は次の通りです:

1. URL `https://example.com/pthc-book` とパスワード `16574894` を受け取ります。
1. `https://example.com/pthc-book` にアクセスして、パスワードの入力画面を表示します。
1. パスワードを入力してボタンを押すと、 `https://example.com/pthc-book/16574894` というURLに移動します[^formjs]。
1. `https://example.com/pthc-book/16574894` が `https://example.com/pthc-book/BvTR3rPt/book.pdf` にリライトされます。
1. `book.pdf` をダウンロードできます。

[^formjs]: formで実装すると `https://example.com/pthc-book/?password=16574894` のようなURLになってしまうため、JavaScriptなどで個別に処理する必要があります。

`https://example.com/pthc-book/BvTR3rPt/book.pdf` にリライトされるルールを複数設定することで、ホスティングサービスの上限まではいくらでもパスワードを発行可能です。

このダウンロードシステムの問題点は、大きく分けて3つあります。1つは単体でダウンロード可能なURLが履歴に記録されること、もう1つはパスワードが非常に効率よく検証できること、そしてパスワードがリライトルール上に平文で記録されていることです。1つ目はURLに秘密情報を含むあらゆる配布形式の問題であり、後の2つはリライトルールを利用する際の問題といえます。

## 実装のこと（Cloudflare Workers+KV+R2の場合）

そのため、安全なダウンロードシステムを実装するには、秘密情報をURLではなくリクエストボディで送信する（典型的にはformでPOSTする）必要があります。この場合、静的サイトを配信する単純なホスティングサービスでは限界があり、個別にダウンロード用のサーバーを立てたり、[Cloud Functions for Firebase](https://firebase.google.com/docs/functions)や[Lambda@Edge](https://aws.amazon.com/jp/lambda/edge/)などのサービスと組み合わせてパスワード照合やコンテンツ提供を行わなければなりません。

今回、光速感情デラックスのダウンロードシステムを実装するにあたっては、[Cloudflare Workers](https://developers.cloudflare.com/workers/)を採用しました。関連するコンポーネントとして、秘密情報とダウンロードできるファイルの対応を記録するための[Workers KV](https://developers.cloudflare.com/workers/learning/how-kv-works/)と、PDFファイルを保管するための[Cloudflare R2](https://developers.cloudflare.com/r2/)を利用しています。

本来は、2022年5月に発表された[D1](https://blog.cloudflare.com/introducing-d1/)を使って最新技術でワイワイ感を出していきたかったのですが、間に合わなかったので素直にKVを使用してありふれたアプリケーションに仕上げました。

ここからは、このQuettaと名付けられたダウンロードシステム[^quetta]でどのように前述の問題を解決できたかを検討しながら、Quettaの特徴について述べていきます。

[^quetta]: コードネーム（レポジトリの名前）はfermitaj-storage、サービス名は「Quetta : クエタ」にしました。

[![Quettaのスクリーンショット](/images/bunfree34/quetta.png){width=600 height=400}](https://quetta.hentaigirls.net/)

### 1: 単体でダウンロード可能なURLが履歴に記録されること

QuettaのURL設計は非常に単純です。`/` 以降のパスは全て推測可能なファイル名として扱われます。パスの一部に秘密情報を含んだり、クエリパラメータで秘密情報を受け取ることはありません。どのようなファイルをダウンロードしたかという履歴は残るかもしれませんが、これは秘密情報が残る懸念とは関係のない問題です。

-----

たとえば、次のようなURLを利用できます: [`https://quetta.hentaigirls.net/shirako2.png`](https://quetta.hentaigirls.net/shirako2.png)

### 2: パスワードが非常に効率よく検証できること

Quettaでは、[bcrypt](https://ja.wikipedia.org/wiki/Bcrypt)でハッシュ化されたパスワードを検証して認証・認可を行います。bcryptもArgon2と同様に総当たり攻撃に強くなるように設計されたアルゴリズムです。本来はArgon2を利用したかったのですが、Cloudflare Workersの環境ではどうしても利用できなかったので諦めました。Argon2に対応するライブラリをWorkersで使ったことがある方は、ぜひ詳細を教えてください。

ここでいう認証とは、受け取ったパスワードをKVに記録されたパスワードと比較することを指しています。認可とは、URLのファイル名をそのパスワードが利用できるファイル名と比較することを指しています。今後、複数のファイルを配布することを意識して、パスワード用のバケットと利用できるファイル名を管理するバケットを分けました。

また、[タイミング攻撃](https://ja.wikipedia.org/wiki/%E3%82%BF%E3%82%A4%E3%83%9F%E3%83%B3%E3%82%B0%E6%94%BB%E6%92%83)を回避するために、Workers KV上のパスワードと一致しなかった場合や、ファイルが利用できない場合など、本来はパスワードハッシュを検証する必要がない（すぐに404を返せる）場合でも必ず検証を行っています。もちろん、ファイルの所在やパスワードの一致不一致に関係なく、ファイルを利用できない場合は全て[404 Not Found](https://developer.mozilla.org/ja/docs/Web/HTTP/Status/404)を返します[^github404]。

[^github404]: これは[GitHub](https://github.com/amane-katagiri/pthc-book)などと同様のスタイルです。

なお、パスワードをどのようにKVに記録し、どのようにACLと対応づけているのか気になった人もいるでしょう。正直に言うと、今回はパスワードの前半部分をIDとして利用するという苦肉の策を採用しています。変更用トークンは英数字20文字で、後半の16文字だけでも十分な強度を保てるという計算です。次回以降は、パスワードに対応するIDをきちんと発行して認証できるようにする予定です。

-----

たとえば、上記のURLで次のようなパスワードを利用できます: `XXXXXXXXXXXXXXXXXXXX`

### 3: パスワードがリライトルール上に平文で記録されていること

前述の通り、パスワードはbcryptでハッシュ化されてからWorkers KVに保管されます。Cloudflare上のどこにも秘密情報（全体）は保存されていません。仮にKVの内容が流出したとしても、総当たりにはなかなかの時間がかかるでしょう。

-----

たとえば、上記のURLとパスワードで次のような画像をダウンロードできます:

[![白子アイコンの画像](/images/bunfree34/shirako2_thumb.png){width=240 height=240}](/images/bunfree34/shirako2.png)

"""この画像は何らかのライセンスの下で提供されているものではありません。"""

# まとめ

* 電子版の同人誌を配布するには、メディア配布方式・ダウンロードリンク方式・電子書籍ストア方式を利用できます。
* 光速感情デラックスの配布では、一般的なダウンロードリンク方式とは異なる要件を満たす必要がありました。
* 1つのファイルを複数のパスワードで安全にダウンロードできるように、Cloudflare Workers+KV+R2上でダウンロードシステムを実装しました。
* 面白くてためになるお話が収録された「[光速感情デラックス](https://bunfree34.hentaigirls.net/)」が発行されました。
