LoginSignup
3
2

More than 3 years have passed since last update.

iOS: DeviceCheckの参照、保存をJavaで実装。(アカウントBan,リセマラ防止対応など)

Posted at

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を端末で取得し、サーバーサイドに渡す。

参考記事

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トークンの取得処理

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;
}
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2