閲覧者のユーザーエージェントを三角関数で判定できるか？

# 概要
先日ふと自分のPCの[フィンガープリント](https://contents.saitolab.org/fingerprint/)を取ってみたところ「IPアドレス」など様々な項目が並ぶ中に「**Math.tan**」という変な項目を見つけました。

「なぜ三角関数が出てくるの？」と気になって調べてみたところ、**三角関数の値はブラウザやOSの実装により微妙に異なることがあり、特定の式をブラウザに計算させることで利用者を識別する手段になり得る[^1]**という話でした。

面白そうだなと思ったので、本記事ではその手法で実際どの程度までブラウザ/OSを判別できるのか調査してみました。

# 検証方法

今回は様々な文献[^1][^2]の情報を参考に、以下の式を各OSの各ブラウザに計算させました。

- `tan(-1e300)`
- `cosh(10)`（厳密には三角関数の類似ですが）

これら以外も10数種類ほど試したのですが、判別に使えたのはこの2つのみでした。

##### 試したOSとバージョン
- macOS Catalina (ver.10.15.6)
- Windows 10 (OSビルド 19041.804)
- iOS (ver.14.4)
- Android (ver.11)

##### 試したブラウザ（各最新版）
- Google Chrome
- Mozilla Firefox
- Safari
- Vivaldi
- Microsoft Edge
- IE11

# 結果

### macOS

|ブラウザ              |Math.tan(-1e300)   |Math.cosh(10)     |
|:--                  |--:                |--:               |
|Chrome, Edge, Vivaldi|-1.4214488238747245|11013.232920103324|
|safari               |-1.4214488238747247|11013.232920103323|
|Firefox              |-1.4214488238747247|11013.232920103324|

**末尾の桁**に着目してください。
どうやらブラウザのJSエンジンごとに異なる計算結果が出ているようです。

### Windows 10

|ブラウザ                       |Math.tan(-1e300)         |Math.cosh(10)     |
|:--                           |--:                      |--:               |
|Chrome, Firefox, Edge, Vivaldi|-1.4214488238747245      |11013.232920103324|
|IE11                          |-4.987183803371025       |非対応😌           |

モダンブラウザは全て同一の値で、IE のみハチャメチャな結果となりました。

### iOS

|ブラウザ                       |Math.tan(-1e300)         |Math.cosh(10)     |
|:--                           |--:                      |--:               |
|Chrome, Firefox, Edge, Safari |-1.4214488238747243      |11013.232920103323|

全ブラウザで同じ値となりました。
[iOS は全てのブラウザが Webkit ベース](https://qiita.com/pink/items/a54b8cadbe39a06956a9)らしいので、これは納得しやすい結果です。

### Android

|ブラウザ名称                    |Math.tan(-1e300)         |Math.cosh(10)     |
|:--                           |--:                      |--:               |
|Chrome, Firefox, Edge, Vivaldi|-1.4214488238747245      |11013.232920103324|

こちらも何故か iOS 同様全ブラウザで共通の結果となりました（値自体は iOS と異なっています）。

# まとめ

- macの結果を見て「おっ、JSエンジンの判別ができるのかな？」と思ったけどそんなことはなかった
- モバイルに限定すると、ブラウザごとの差異はなかったがOSの判別はできた

完全な判別はできなかったものの、ユーザーエージェント文字列と異なり偽装等の問題も少なそうなので補助的な特徴点としては使えるのかもしれません（ただ情報量としてはそれほど大きくないようです[^2]）。

ちなみにいまお使いのブラウザによる計算結果は以下です。

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="result" data-user="POPOPON" data-slug-hash="JjXRJvE" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Trigonometry Fingerprint">
  <span>See the Pen <a href="https://codepen.io/POPOPON/pen/JjXRJvE">
  Trigonometry Fingerprint</a> by Masa-Shin (<a href="https://codepen.io/POPOPON">@POPOPON</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

今回調査していないOS/ブラウザをお使いの方はぜひ結果をコメント等で教えてください。

# 補遺A： ブラウザ/OS等により値が異なる背景

[EcmaScript の仕様](https://262.ecma-international.org/10.0/#sec-math.tan)では、例えば`Math.tan(x)`は「実装に依存した近似値」を返すと定義されています。
つまり具体的にどういう手法で計算し誤差をどう丸めるかについては何も規定されていません。

また IEEE 754 標準も三角関数の（実装は推奨しているものの）計算手法自体については特に規定していないかと思います<sup>[[要出典](https://ja.wikipedia.org/wiki/Wikipedia:%E3%80%8C%E8%A6%81%E5%87%BA%E5%85%B8%E3%80%8D%E3%82%92%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%81%95%E3%82%8C%E3%81%9F%E6%96%B9%E3%81%B8)]</sup>。

そのためブラウザ/OSが三角関数の計算や端数処理をどう実装しているかにより、返り値が変わる事態になってしまっているようです。

#### 追記：CPUも関係していそう

某サイトで「macOSでもM1チップだと`tan`の値が違った」という興味深いコメントを目にしました。
また 32bit CPU の場合も値が異なるらしいので、ブラウザ/OSなどソフトウェア面だけではなくハードウェア面の性質も捉えた特徴点となっているようです。

関連ツイート
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ブラウザ(JS)が、もあるけど、CPUが、というのもあるかと思う。<br>浮動小数点レジスタのビット数とか、丸めモードのフラグとか。<br><br>【閲覧者のユーザーエージェントを三角関数で判定できるか？ - Qiita】 <a href="https://t.co/ElrdzPlId1">https://t.co/ElrdzPlId1</a></p>&mdash; Justy (@i_justy) <a href="https://twitter.com/i_justy/status/1362075308778614790?ref_src=twsrc%5Etfw">February 17, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 補遺B： 実際の値は？（改稿版）

任意精度計算ができるという Python パッケージ [mpmath](https://mpmath.org/) を利用し計算してみました。

```python:コード
from mpmath import mp

mp.dps = 300 # 有効桁数
print(mp.tan("-1e300"))
print(mp.cosh(10))
```

（この節のコードは[kasai氏](https://twitter.com/mkashi/status/1361642857245728773)の助言を受け改稿しています。ありがとうございました）

- 有効桁数を`300`にしているのは、実際に必要な桁数よりも有効桁数を大きくした方が正確な値が求まりやすいためです（[mpmathのドキュメント](https://mpmath.org/doc/current/technical.html#correctness-guarantees)）。

- `-1e300`は桁数が大き過ぎるため mpmath では文字列として渡す必要があります。

**結果**

|Math.tan(-1e300) |Math.cosh(10) |
|--: |--: |
|-5.8600819259448981 |11013.232920103323|

- `Math.tan(-1e300)`はブラウザが計算した値と大きく異なるものとなりました。
- `Math.cosh(10)`については「macOS の safari」および「iOS の各ブラウザ」が見事ピタリ賞を獲得しました🎉

# 参考文献

- [Web Browser Fingerprint解説ページ](https://www.saitolab.org/fp_site/)
- [JavaScriptの数値計算はどれくらい正確なのか - Zenn](https://zenn.dev/uhyo/articles/javascript-math-accuracy)

[^1]: [Browser Fingerprintingにおける特徴の組み合わせに関する考察](https://ipsj.ixsq.nii.ac.jp/ej/index.php?active_action=repository_view_main_item_detail&page_id=13&block_id=8&item_id=187330&item_no=1)

[^2]: [Browser fingerprinting - Comenius University, Bratislava](http://www.dcs.fmph.uniba.sk/diplomovky/obhajene/getfile.php/hraska_diploma_thesis.pdf?id=434&fid=771&type=application%2Fpdf)