iOSのdevice checkで出来ること。
- 端末から取得できるdevice tokenを元に、端末単位で2つのビット値をAppleのAPIに記録、参照できる。
- 端末を初期化、アプリのアンインストール&インストールしても保存されたビット値はリセットされない。
どんなことに利用できるか。
リセマラ防止。
- (初期インストール時、device checkのAPIに記録すると、再インストールされたことを判定できる。)
深刻な不正行為をするユーザーを端末単位で、アカウントbanする。
- device checkのAPIで、アカウントbanされた端末であることを記録。
- アプリの再インストール、端末の初期化後も、以前にアカウントbanされた端末である場合、利用できないようにする。
利用時の注意点
- 端末が中古市場等で別の人に渡った場合でも、保持される値なので慎重な運用が必要。
- 端末から取得したdevice tokenは時間的な有効期限があるみたい。(この部分の記憶は定かじゃないので、テストしてみてください。)アカウントbanで利用する場合は、banした後の次のアプリ起動時にdevice tokenをサーバーサイドに送って、そこですぐにdevice checkのAPIで更新するような工夫が必要。
事前準備、前提
- 認証キーのp8ファイルを取得。
- KEY_ID, TEAM_IDを取得
- DeviceTokenを端末で取得し、サーバーサイドに渡す。
参考記事
-
iOS11で追加されたDeviceCheckについて
-
apple 公式:
DeviceCheck APIのURL
baseのAPI
開発
本番運用時
APIの機能
参照:query_two_bits
更新:update_two_bits
device tokenのバリデーション:validate_device_token
プログラム
iOSアプリでdevice tokenを取得。
※シミュレーターでは取得できません。
import DeviceCheck
DCDevice.current.generateToken {
(data, error) in
guard let data = data else {
return
}
let token = data.base64EncodedString()
print(token)
}
サーバーサイド(Java)
取得、更新サンプル
// 取得
private void queryIOSDeviceCheckSample() {
// この値で2つのbool値と最終更新日時を取得できる。
Response response = postRequest(DEVELOPMENT_BASE_API_URL + "query_two_bits", "端末から取得したdevicetoken", null, null);
}
// 更新
private void updateIOSDeviceCheckSample(Boolean bit0, Boolean bit1) {
Response response = postRequest(DEVELOPMENT_BASE_API_URL + "update_two_bits", "端末から取得したdevicetoken", null, null);
}
API呼び出し処理
- "Authorization"ヘッダーにJWTのトークンを設定。
- request bodyにjson形式で各パラメーターを設定。
- device_token, transaction_id, timestamp, bit0, bit1
private static Response postRequest(String url, String deviceToken, Boolean bit0, Boolean bit1) throws IOException {
MediaType JSON = MediaType.get("application/json; charset=utf-8");
JSONObject jsonObject = new JSONObject();
jsonObject.put("device_token", deviceToken);
jsonObject.put("transaction_id", UUID.randomUUID().toString());
jsonObject.put("timestamp", new Date().getTime());
if (bit0 != null) {
jsonObject.put("bit0", bit0);
}
if (bit1 != null) {
jsonObject.put("bit1", bit1);
}
String json = jsonObject.toJSONString();
RequestBody body = RequestBody.create(JSON, json);
String jwt = getJWTStr();
if (jwt == null) {
return null;
}
Request request = new Request.Builder()
.url(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Content-Length", String.valueOf(json.length()))
.header("Authorization", "Bearer " + jwt)
.post(body)
.build();
OkHttpClient client = new OkHttpClient();
return client.newCall(request).execute();
}
p8ファイルから、JWTトークンを取得。
プッシュ通知用ライブラリ「pushy」のソースを参考に実装:JWTトークンの取得処理
- pushyという、iOSのプッシュ通知用のライブラリがあり、
p8ファイルからJWTトークンを取得する処理がある。 - その部分を参考にして実装。
https://github.com/jchambers/pushy/blob/master/pushy/src/main/java/com/eatthepath/pushy/apns/auth/ApnsSigningKey.java#L124-L170
JTW(=JSON Web Token)
private static String getJWTStr() {
try {
ECPrivateKey privateKey = getECPrivateKey(P8_SECRET_KEY_PATH);
return Jwts.builder()
.setHeaderParam("kid", KEY_ID)
.setIssuer(TEAM_ID)
.setIssuedAt(new Date())
.signWith(privateKey, SignatureAlgorithm.ES256)
.compact();
} catch (Exception e) {
return null;
}
}
private static ECPrivateKey getECPrivateKey(String p8FilePath) throws Exception {
final FileInputStream fileInputStream = new FileInputStream(new File(p8FilePath));
final ECPrivateKey signingKey;
{
final String base64EncodedPrivateKey;
{
final StringBuilder privateKeyBuilder = new StringBuilder();
final BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
boolean haveReadHeader = false;
boolean haveReadFooter = false;
for (String line; (line = reader.readLine()) != null; ) {
if (!haveReadHeader) {
if (line.contains("BEGIN PRIVATE KEY")) {
haveReadHeader = true;
}
} else {
if (line.contains("END PRIVATE KEY")) {
haveReadFooter = true;
break;
} else {
privateKeyBuilder.append(line);
}
}
}
if (!(haveReadHeader && haveReadFooter)) {
throw new IOException("Could not find private key header/footer");
}
base64EncodedPrivateKey = privateKeyBuilder.toString();
}
final byte[] keyBytes = Base64.getDecoder().decode(base64EncodedPrivateKey.getBytes(StandardCharsets.US_ASCII));
final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
final KeyFactory keyFactory = KeyFactory.getInstance("EC");
try {
signingKey = (ECPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
throw new InvalidKeyException(e);
}
}
return signingKey;
}