Web Audio APIを使ったブラウザフィンガープリント手法の紹介

[Web Audio API](https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API)というAPIがあります。
[RFC](https://www.w3.org/TR/webaudio/) / [日本語訳](https://g200kg.github.io/web-audio-api-ja/index.html)

ざっくり言うと、ブラウザで音を作ったり出したりできるAPIです。

RFCの時点で[フィンガープリントの懸念が大量に書かれてる](https://www.w3.org/TR/webaudio/#priv-sec)という有様ですが、実際に実用レベルでブラウザフィンガープリントしている例が存在しました。
[FingerprintJS](https://fingerprintjs.com/)という、そのまんまな名前のライブラリです。

以下は同ライブラリのブログより、[How the Web Audio API is used for browser fingerprinting](https://fingerprintjs.com/blog/audio-fingerprinting/)という記事のざっくり紹介です。


# How the Web Audio API is used for browser fingerprinting

Cookieを使うこともなく、許可を求めることもなく、Webブラウザを識別できることを知っていますか？
この技術は`ブラウザフィンガープリント`と呼ばれていて、ブラウザの属性を読み取り、まとめて一つの識別子にすることによって識別します。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/48aae237-445c-3e56-6adb-3be9ce477648.png)


ブラウザフィンガープリントの生成方法には色々なものがありますが、今回取り上げる独創的なテクニックが、オーディオフィンガープリントです。
オーディオフィンガープリントは比較的ユニークで、そして安定しているため、貴重な情報源です。
ユニークである理由は、Web Audio APIが複雑で精巧であり、オーディオソースが数学的に生成されたものであるからです。

実装に入る前に、Web Audio APIのアイデアをいくつか理解しておく必要があります。

## A brief overview of the Web Audio API

Web Audio APIはオーディオの操作を行うパワフルなAPIです。
ひとつのAudioContextで複数のソースや処理をチェーン実行することができます。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/f34729e9-63f4-97e3-5df2-6827d73c7ca4.png)


ソースとしてはaudio要素、stream、そしてOscillatorから数学的に生成されたインメモリソースなどがあります。
ここではソースとしてOscillatorを使います。

### AudioContext

AudioContextは処理の全体を表し、複数のノードをリンクして実行を制御します。
AudioContextは最初にひとつだけ生成し、その後はそれを使い回します。

特殊なAudioContextとしてOfflineAudioContextがあり、これは生成した音を再生することなく[AudioBuffer](https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer)メモリ上に保存します。

```js
const AudioContext = 
  window.OfflineAudioContext ||
  window.webkitOfflineAudioContext
const context = new AudioContext(1, 5000, 44100)
```

### AudioBuffer

AudioBufferはメモリに保存された小さな音声ファイルです。
複数のチャンネルを格納することができますが、ここでは1チャンネルだけ利用します。


### Oscillator

Oscillatorは周期的な波形を生成する最も簡単な方法で、デフォルトは正弦波です。
矩形波、三角波など他の形を生成することも可能です。

### Compressor

Web Audio APIにはDynamicsCompressorNodeというCompressorが用意されています。
DynamicsCompressorNodeには多くのプロパティがありますが、ここではその一部を利用します。


## How the audio fingerprint is calculated

必要な概念が揃ったので、オーディオフィンガープリントを作ってみましょう。
1チャンネル、フレーム数5000、サンプリングレート44100HzのAudioContextを作成します。
これは113ミリ秒の長さになります。

```js
const AudioContext =
  window.OfflineAudioContext ||
  window.webkitOfflineAudioContex
const context = new AudioContext(1, 5000, 44100)
```

次にoscillatorで、音源となる1秒間に1000回の三角波を生成します。

```js
const oscillator = context.createOscillator()
oscillator.type = "triangle"
oscillator.frequency.value = 1000
```

さらにCompressorを作成し、信号を変換しましょう。

```js
const compressor = context.createDynamicsCompressor()
compressor.threshold.value = -50
compressor.knee.value = 40
compressor.ratio.value = 12
compressor.reduction.value = 20
compressor.attack.value = 0
compressor.release.value = 0.2
```

それぞれのノードを接続します。

```js
oscillator.connect(compressor)
compressor.connect(context.destination);
```

完成したら`oncomplete`を使って結果を取得します。

```js
oscillator.start()
context.oncomplete = event => {
  const samples = event.renderedBuffer.getChannelData(0)
};
context.startRendering()
```

`samples`は音を表す浮動小数の配列です。
ここからひとつの値を生成する必要がありますが、ここでは単純に全ての値を合計してみましょう。

```js
function calculateHash(samples) {
  let hash = 0
  for (let i = 0; i < samples.length; ++i) {
    hash += Math.abs(samples[i])
  }
  return hash
}

console.log(getHash(samples))
```

これでオーディオフィンガープリントの完成です。

> 101.45647543197447

MacOSのChromeで実行すると、このような数字が出てきました。

[オープンソースのライブラリ](https://github.com/fingerprintjs/fingerprintjs/blob/3201a7d61bb4df2816c226d8364cc98bb4235e59/src/sources/audio.ts)で、実際の実装を確認することができます。

これをSafariで実行すると、別の値が出てきました。

> 79.58850509487092

Firefoxだとまたさらに別の値になりました。

> 80.95458510611206

全てのブラウザで、異なる値になりました。
この値は何度計算しても安定していて、シークレットモードにおいても同じ値になります。

この値はハードウェアとOS、ブラウザに依存します。


## Why the audio fingerprint varies by browser

なぜブラウザによって値が異なるのか見てみましょう。

ChromeとFirefoxで、単一の三角波を出力したときの波形は本来はこのようになるはずです。

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/351fab65-3f0b-77ed-b5d1-6eb94e51e0d4.png)


しかし実際に出力される形はOSやブラウザによって異なります。

![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/63380c92-16b5-be38-0a22-18cc1122b931.png)


主要なブラウザエンジン(Blink・WebKit・Gecko)は、Googleが2011年ごろに開発したコードをベースにWeb Audio APIを実装しました。
その後ブラウザごとに多くの変更が加わったため、オーディオフィンガープリントにはそれぞれ違いが生じました。

最近の実装は以下で見ることができます。

Blink: [oscillator](https://github.com/chromium/chromium/blob/9841ee86b710dc649cf41772f560600324cadf45/third_party/blink/renderer/modules/webaudio/periodic_wave.cc#L468) / [dynamics compressor](https://github.com/chromium/chromium/blob/3e914531a360b766bfd8468f59259b3ab29118d7/third_party/blink/renderer/platform/audio/dynamics_compressor_kernel.cc#L202)
WebKit: [oscillator](https://github.com/WebKit/WebKit/blob/010d252ab89d2c867efcba547e879c11968eebe7/Source/WebCore/Modules/webaudio/PeriodicWave.cpp#L250) / [dynamics compressor](https://github.com/WebKit/WebKit/blob/010d252ab89d2c867efcba547e879c11968eebe7/Source/WebCore/platform/audio/DynamicsCompressorKernel.cpp#L188)
Gecko: [oscillator](https://github.com/mozilla/gecko-dev/blob/9ae77e4ce3378bd683ac9a86b729ea6b6bd22cb8/dom/media/webaudio/blink/PeriodicWave.cpp#L286) / [dynamics compressor](https://github.com/mozilla/gecko-dev/blob/9ae77e4ce3378bd683ac9a86b729ea6b6bd22cb8/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp#L213)


## Pitfalls

オーディオフィンガープリントの実用にあたって、我々はブラウザの互換性、安定性、パフォーマンスの向上を目指しました。
互換性として、TorやBraveといったプライバシーに特化したブラウザにも注力しました。


### OfflineAudioContext

OfflineAudioContextは[ほとんどの環境で利用できます](https://caniuse.com/mdn-api_offlineaudiocontext)が、一部特別な処理が必要なケースもあります。

まずiOS11以前では、OfflineAudioContextは[ユーザアクションでのみトリガー可能](https://stackoverflow.com/questions/46363048/onaudioprocess-not-called-on-ios11/46534088#46534088)です。
このバージョンを未だに使っているユーザはほとんどいないので、サポート対象から外しました。

ついでiOS12以降では、ページがバックグラウンドに行ったときに処理が拒否される場合があります。
幸いフォアグラウンドに戻れば再開できるので、`context.startRendering()`を何度も試みるようにしました。


### Tor

これはシンプルです。
Torでは[Web Audio APIが無効化されている](https://gitlab.torproject.org/legacy/trac/-/issues/21984)ため、オーディオフィンガープリントは使えません。


### Brave

Braveでの状況は微妙です。
Braveは、Blinkから派生したプライバシー重視のブラウザです。
このブラウザは、サンプリング値をわずかに変動させる仕組みが備わっており、これは`farbling`と呼ばれています。
`farbling`は3レベルがあり、ユーザは好きな設定を選ぶことができます。

- Disabled : farblingしない。他のブラウザと同じ。
- Standard : デフォルト。セッションごとに決められたランダム値(fudgeと呼ばれる)増減する。
- Strict : 音波が疑似乱数に置き換えられる。

fudgeは取得できるので、Standardの設定であればfarblingする前の値に戻すことができます。


## Audio fingerprinting is only a small part of the larger identification process.

オーディオフィンガープリントは、我々のライブラリがブラウザフィンガープリントを生成するために使用する、多くの情報源のひとつにすぎません。
全てのフィンガープリントを盲目的に受け入れず、それぞれの安定性やカーディナリティを個別に分析し、精度を判断しています。

オーディオフィンガープリントはカーディナリティは高くないものの、安定性が高いため、フィンガープリントの精度を向上することができます。


## Try Browser Fingerprinting for Yourself

ブラウザフィンガープリントは、訪問者の識別方法として有効です。
特に、Cookieの消去、シークレットモード、VPNの使用などでトラッキングを避けようとする"maliciousな"訪問者を特定することに役立ちます。

我々の[オープンソースライブラリ](https://github.com/fingerprintjs/fingerprintjs)を用いて独自にブラウザフィンガープリントを実装することもできます。

我々はまた、さらに高い精度を誇る[FingerprintJS Pro API](https://fingerprintjs.com/)を開発しました。
月間1000人までは[無料で使用できます](https://dashboard.fingerprintjs.com/signup)。


# 感想

Web Audio APIはパーミッションが不要です。
ユーザリアクションなしに音を出すことは[許されていません](https://developer.mozilla.org/ja/docs/Web/Media/Autoplay_guide#autoplay_availability)が、逆に言えば音さえ出さなければなんでもできるということです。
すなわち、あなたのブラウザは既にこれらの技術で密かにフィンガープリントされているということです。

この技術はあくまでブラウザフィンガープリントであって、ブラウザを識別するためには使えても、それ以上の情報を得ることはできません。
そのブラウザを誰が使っているのか、複数のブラウザの紐付けといった情報なんかはわからないということですね。

だったら安全じゃないか？
そのとおりです。
この技術が、単独で使われているかぎりは。

[その他の技術](https://qiita.com/rana_kualu/items/e470f0303e6b3a429c09)と組み合わせることで、一気に危ない技術になります。