iOS11で追加されたDeviceCheckについて

**本記事は公開済みの[WWDC2017 - Privacy and Your Apps](https://developer.apple.com/videos/play/wwdc2017/702/)と[Apple Developerのドキュメント](https://developer.apple.com/documentation/devicecheck)を元に作成しています。記事の内容はベータ版時によるものなので、今後仕様変更などにより異なる場合があります。予めご了承ください。**
*また記事にミスなどがありましたらご指摘頂けると助かります…*

---
iOS11からDeviceCheckというフレームワークとサーバーサイドのAPIが追加されました。これらを利用することでデバイス毎に紐づけたい情報を簡単に設定・取得することができます。

# DeviceCheckとは
![devicecheck_1.png](https://qiita-image-store.s3.amazonaws.com/0/186419/90e61d06-b081-6b76-87b6-3865d3ee6b28.png)
*[WWDC2017 Privacy and Your Apps](https://developer.apple.com/videos/play/wwdc2017/702/)より引用*

- このデバイスで以前フリートライアルを利用したことがあるか？
- 課金済みアカウントではないが、このデバイスで以前支払いを行ったことがあるか？
- 不正なユーザーがこのデバイスを利用したことがあるか？

...といった、デバイス単位での制限状態や利用状態を確認したいときに活躍します。

デバイスの識別といえば、今まではUDID、MACアドレス、Push通知のトークンなど、ありとあらゆる固有識別子を利用していた頃もありましたが、いずれもAppleによって対策されて使えなくなりました。
現在ではUUIDを生成しキーチェーンに保存する方法や、IDFV(Identifier for Vendor)を使うことで識別はできますが、アプリの削除、初期化(復元)によって識別子が消えてしまいます。
DeviceCheckは、2ビット(最大4つの表現)とタイムスタンプをAppleのサーバーに記録します。これにより**アプリの削除、デバイスの復元やリセットで記録が消えてしまうことがありません。**

# おおまかな流れ
アプリインストールが初めての場合
![devicecheck_2.png](https://qiita-image-store.s3.amazonaws.com/0/186419/07d94c52-9906-88d7-59ec-1f9f8aaeffd5.png)
*[WWDC2017 Privacy and Your Apps](https://developer.apple.com/videos/play/wwdc2017/702/)より引用*

アプリ削除、または再インストールの場合
![devicecheck_3.png](https://qiita-image-store.s3.amazonaws.com/0/186419/d11b9cf7-6ee3-2cdd-0297-d669827d51b3.png)
*[WWDC2017 Privacy and Your Apps](https://developer.apple.com/videos/play/wwdc2017/702/)より引用*


最初にクライアント側で[generateToken](https://developer.apple.com/documentation/devicecheck/dcdevice/2902276-generatetoken)を呼び出し、一時的トークンを取得します。
次に、このトークンをAPIサーバーに投げます。サーバー側では秘密鍵を利用してJWT(JSON Web Token)を生成。クライアントから受け取ったトークンとトランザクションID、タイムスタンプをJSONエンコードしAppleのAPIに投げてデバイスに紐づく情報を取得、または設定を行います。
情報を取得した場合はサーバー側で動作を許可するかしないかといった判定を行います。

# サーバーサイド側の実装例
実装にあたって、事前にApple Developerで秘密鍵をもらってくる必要があります。鍵は[Certificates, Identifiers & ProfilesのKeys](https://developer.apple.com/account/ios/authkey/)から作成できます。
鍵の作成にはCSRが必要です。[こちらを参考に](http://qiita.com/takuyaWt/items/50f3a1e549cca987d003)作成してください。
注意点として、**秘密鍵は再ダウンロードできません**。鍵をなくしたり漏洩した可能性がある場合は、Revokeして再作成してください。

これらに加えて、JWTを生成する際に

- 秘密鍵のID(Keysから見られます)
- 開発アカウントのTeamID

が必要です。こちらも事前に調べておいてください。


準備ができましたら[Appleのドキュメントを参考に](https://developer.apple.com/documentation/devicecheck/accessing_and_modifying_the_per_device_data)サーバーサイド側の実装を行います。ここではPHP7を利用します。

```bash:composer
composer require zenstruck/jwt ramsey/uuid
```

```php:requestToken.php
<?php
require_once "vendor/autoload.php";
use Zenstruck\JWT\Token;
use Zenstruck\JWT\Signer\OpenSSL\ECDSA\ES256;
use \Ramsey\Uuid\Uuid;

//	_POSTからdeviceTokenを受け取り
$deviceToken = (isset($_POST["deviceToken"]) ? $_POST["deviceToken"] : null);

function generateJWT($teamId, $keyId, $privateKeyFilePath) {
	$payload = [
		"iss" => $teamId,
		"iat" => time()
	];

	$header = [
		"kid" => $keyId
	];

	$token = new Token($payload, $header);
	return (string)$token->sign(new ES256(), $privateKeyFilePath);
}

function postRequest($url, $jwt, $bodyArray) {
	$body = json_encode($bodyArray);

	$header = [
		"Authorization: Bearer ". $jwt,
		"Content-Type: application/x-www-form-urlencoded",
		"Content-Length: ".strlen($body)
	];

	$context = [
		"http" => [
			"method"  => "POST",
			"header"  => implode("\r\n", $header),
			"content" => $body
		]
	];

	return file_get_contents($url, false, stream_context_create($context));
}

$teamId = "TEAMID";
$keyId = "KEYID";
$privateKeyFilePath = "PRIVATE KEY FILE PATH";
$jwt = generateJWT($teamId, $keyId, $privateKeyFilePath);

//	情報の取得
$body = [
	"device_token" => $deviceToken,
	"transaction_id" => Uuid::uuid4()->toString(),
	"timestamp" => ceil(microtime(true)*1000) // time()だとだめでした
];
postRequest("https://api.development.devicecheck.apple.com/v1/query_two_bits", $jwt, $body);
//	まだ情報を設定していない場合は、"Failed to find bit state"と返されます。
//	設定している場合はこんな形で帰ってきます。
//	{"bit0":true,"bit1":false,"last_update_time":"2017-06"}


//	情報の設定
$body = [
	"device_token" => $deviceToken,
	"transaction_id" => Uuid::uuid4()->toString(),
	"timestamp" => ceil(microtime(true)*1000),
	"bit0" => true,
	"bit1" => false
];

postRequest("https://api.development.devicecheck.apple.com/v1/update_two_bits", $jwt, $body);
```

サーバーサイドの実装例は以上です。状況に応じて、取得した値の判定処理やアップデートを行ってください。


# クライアントサイドの実装例
DeviceCheckフレームワークをインポートして利用します。
ここではAlamofireを利用してトークンを投げています。
ちなみにデバッグ先がシミュレータだとビルドできませんでしたので、実機に向けてください。

```swift:ViewController.swift
import UIKit
import DeviceCheck
import Alamofire

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.requestToken()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    fileprivate func requestToken() {
        DCDevice.current.generateToken {
            (data, error) in
            guard let data = data else {
                return
            }

            let token = data.base64EncodedString()
            //  トークンをサーバーに投げる
            let url = "https://hogehogehoge.com/v1/request_token"
            let params = ["deviceToken": token]
            Alamofire.request(url, method: .post, parameters: params).responseString {
                (request) in
                //  ここで処理
                print(request.value)
            }
        }
    }
}
```

# 気を付けること
- デバイスが売買・譲渡されたときの想定をする

DeviceCheckはデバイスの初期化を超えた識別を可能にしますので、デバイスの持ち主が変わったときや、関連づくアカウントが変わったときには情報を更新するといった処理を行う必要があります。DeviceCheck APIから情報を取得した際に最終更新日が取得できますので、これをみて処理を分けることができそうです。
ちなみに最終更新日はYYYY-MMの形式で帰ってきます。日にち・時刻は省かれてますね…

# 感想
DeviceCheck APIを利用することで、簡単にデバイスと情報を紐づけることができました。情報量は少ないですが、それでも通常利用する分には問題ないレベルですし、なによりデバイスを初期化しても情報が紐づいたままというのは大きいと思います。

# 参考
[[iOS 11] 初回起動判定などに使える DeviceCheck フレームワークとは #WWDC17](http://dev.classmethod.jp/smartphone/iphone/ios-11-device-check/)
[Accessing and Modifying the Per-Device Data](https://developer.apple.com/documentation/devicecheck/accessing_and_modifying_the_per_device_data?changes=latest_minor)
[iOSのプッシュ通知送信時にトークン認証が使えるようになったので調べてみた](http://qiita.com/Tueno@github/items/9b4b59a9191b64299cf8)