A fetch
wrapper that hampers traffic analysis, based on Signal's Expanding Signal GIF search article.
This README outlines the high-level ideas, see CONTRIBUTING.md
for information about how to contribute to/build the project.
From Signal's article:[1]
If the Signal service were malicious, it could measure the amount of data being transmitted in order to discern something about the GIFs being retrieved from GIPHY.
The most common way to mitigate an attack like that is through the introduction of plaintext padding. Including a random amount of padding at the end of each GIF would make it more difficult for the Signal service to correlate the amount of data it sees being transmitted with a known GIF.
…
We can also abuse range requests to simulate padding on content we don't control.
This package uses range requests to split a request into segments with some padding.
Using range requests we can turn 1 request for a N₁-byte resource into M requests for N₂-bytes, where:
- N₁ ≥ N₂; and
- (M × N₂) ≥ N₁
(At the time of writing the diagram in the Signal article has inconsistent segment sizes, so there is a different example below.)
Pretend there is a 9-byte resource we want to request (N₁), and we pick a segment size of 4 bytes (N₂).
Instead of making one 9-byte request, we can make three 4-byte requests:
The range requests for the three segments:
Request | Range |
Segment size |
---|---|---|
1 | bytes=0-3 |
4 bytes |
2 | bytes=4-7 |
4 bytes |
3 | bytes=5-8 |
4 bytes |
We have requested the 5th, 6th, and 7th bytes twice and can discard the redundant copy.
This package will use segment sizes a lot larger than 4 bytes, picking the smallest segment size (N₂) from the following list such that N₁ ≥ N₂ holds:
𝑥 | Unit |
---|---|
10 | kibibytes |
50 | kibibytes |
100 | kibibytes |
500 | kibibytes |
1 | mebibyte |
With this approach we turn a request for 9 bytes into a request for 12 bytes, and hide the size of the resource. This makes it more difficult for an intermediary (e.g. a malicious proxy server) to know: is the client requesting three unrelated 4-byte resources or one 12-byte resource? (To which the answer is neither. 😏)
This comes at the cost of multiple extra network calls and bytes on the wire—a steep cost in some scenarios.
NOTE: this package requires a random number generator that will produce uniform numbers, preferably one which is cryptographically sound. One option is pure-random-number
, which is available for both browsers and Node.
To install and use the package:
$ yarn add private-request pure-random-number
import pr from 'private-request';
import randomNumber from 'pure-random-number';
const rng = async (min, max) => randomNumber(min, max);
const fetch = pr({ rng });
// Use as you would `window.fetch`
To run the test suite locally:
$ yarn
$ yarn build:data
$ yarn start:server
$ yarn test
To run the e2e tests, visit localhost:8001
in a browser.
This repository is available under the ISC License. See LICENSE.md
.
The ability to perform range requests on the web requires the correct CORS headers.
Namely:
Access-Control-Allow-Origin
must include the requesting originAccess-Control-Expose-Headers
must includeContent-Range
Remember that the following environments do not enforce CORS:
-
The security model for XMLHttpRequest is different than on web as there is no concept of CORS in native apps.
-
Web extensions
[CORS is not enforced] in the background and popup pages if the extension has those domains in their manifest permissions