무결성 확인 결과

이 페이지에서는 반환된 무결성 확인 결과를 해석하고 사용하는 방법을 설명합니다. 표준 API 요청을 하든 기존 API 요청을 하든 무결성 확인 결과는 비슷한 콘텐츠와 함께 동일한 형식으로 반환됩니다. 무결성 확인 결과는 기기, 앱, 계정의 유효성에 관한 정보를 전달합니다. 앱 서버는 복호화되어 확인된 결과에서 결과 페이로드를 사용하여 앱에서 특정 작업이나 요청을 처리하는 최선의 방법을 결정할 수 있습니다.

반환된 무결성 확인 결과 형식

페이로드는 일반 텍스트 JSON이고 개발자가 제공한 정보와 함께 무결성 신호를 포함합니다.

일반적인 페이로드 구조는 다음과 같습니다.

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: { ... }
}

각 무결성 확인 결과를 확인하기 전에 먼저 requestDetails 필드의 값이 원래 요청의 값과 일치하는지 확인해야 합니다. 다음 섹션에서는 각 필드를 자세히 설명합니다.

요청 세부정보 필드

requestDetails 필드에는 표준 요청의 경우 requestHash에, 기존 요청의 경우 nonce에 개발자가 제공한 정보를 비롯하여 요청에 관한 정보가 포함됩니다.

표준 API 요청:

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the request.
  requestPackageName: "com.package.name"
  // Request hash provided by the developer.
  requestHash: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the integrity token
  // was prepared (computed on the server).
  timestampMillis: "1675655009345"
}

이러한 값은 원래 요청의 값과 일치해야 합니다. 따라서 requestPackageNamerequestHash가 다음 코드 스니펫과 같이 원래 요청에서 전송된 것과 일치하는지 확인하여 JSON 페이로드의 requestDetails 부분을 확인합니다.

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val requestHash = requestDetails.getString("requestHash")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

RequestDetails requestDetails =
    decodeIntegrityTokenResponse
    .getTokenPayloadExternal()
    .getRequestDetails();
String requestPackageName = requestDetails.getRequestPackageName();
String requestHash = requestDetails.getRequestHash();
long timestampMillis = requestDetails.getTimestampMillis();
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request.
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

기존 API 요청:

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the
  // request.
  requestPackageName: "com.package.name"
  // base64-encoded URL-safe no-wrap nonce provided by the developer.
  nonce: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the request was made
  // (computed on the server).
  timestampMillis: "1617893780"
}

이러한 값은 원래 요청의 값과 일치해야 합니다. 따라서 requestPackageNamenonce가 다음 코드 스니펫과 같이 원래 요청에서 전송된 것과 일치하는지 확인하여 JSON 페이로드의 requestDetails 부분을 확인합니다.

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val nonce = requestDetails.getString("nonce")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

JSONObject requestDetails =
    new JSONObject(payload).getJSONObject("requestDetails");
String requestPackageName = requestDetails.getString("requestPackageName");
String nonce = requestDetails.getString("nonce");
long timestampMillis = requestDetails.getLong("timestampMillis");
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

애플리케이션 무결성 필드

appIntegrity 필드에는 패키지 관련 정보가 포함됩니다.

appIntegrity: {
  // PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, or UNEVALUATED.
  appRecognitionVerdict: "PLAY_RECOGNIZED"
  // The package name of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  packageName: "com.package.name"
  // The sha256 digest of app certificates (base64-encoded URL-safe).
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"]
  // The version of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  versionCode: "42"
}

appRecognitionVerdict가 보유할 수 있는 값은 다음과 같습니다.

PLAY_RECOGNIZED
앱과 인증서가 Google Play에서 배포된 버전과 일치합니다.
UNRECOGNIZED_VERSION
인증서나 패키지 이름이 Google Play 레코드와 일치하지 않습니다.
UNEVALUATED
애플리케이션 무결성이 평가되지 않았습니다. 기기를 충분히 신뢰할 수 없는 등 필요한 요구사항을 충족하지 못했습니다.

토큰이 개발자가 만든 앱에서 생성되었는지 확인하려면 다음 코드 스니펫과 같이 애플리케이션 무결성이 예상대로 나타나는지 확인합니다.

Kotlin

val appIntegrity = JSONObject(payload).getJSONObject("appIntegrity")
val appRecognitionVerdict = appIntegrity.getString("appRecognitionVerdict")

if (appRecognitionVerdict == "PLAY_RECOGNIZED") {
    // Looks good!
}

Java

JSONObject appIntegrity =
    new JSONObject(payload).getJSONObject("appIntegrity");
String appRecognitionVerdict =
    appIntegrity.getString("appRecognitionVerdict");

if (appRecognitionVerdict.equals("PLAY_RECOGNIZED")) {
    // Looks good!
}

앱 패키지 이름과 앱 버전, 앱 인증서를 수동으로 확인할 수도 있습니다.

기기 무결성 필드

deviceIntegrity 필드에는 기기가 앱 무결성을 얼마나 잘 시행할 수 있는지 나타내는 라벨이 하나 이상 있는 단일 값 deviceRecognitionVerdict가 포함되어 있을 수 있습니다. 기기가 라벨 기준을 충족하지 않으면 deviceIntegrity 필드는 비어 있습니다.

deviceIntegrity: {
  // "MEETS_DEVICE_INTEGRITY" is one of several possible values.
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
}

기본적으로 deviceRecognitionVerdict에는 다음이 포함될 수 있습니다.

MEETS_DEVICE_INTEGRITY
앱이 Google Play 서비스가 설치된 Android 지원 기기에서 실행 중입니다. 기기는 시스템 무결성 검사를 통과하고 Android 호환성 요구사항을 충족합니다.
비어 있음(빈 값)
앱이 공격(예: API 후킹)이나 시스템 손상(예: 루팅됨) 징후가 있는 기기에서 실행되거나, 앱이 Google Play 무결성 검사를 통과하지 못한 에뮬레이터와 같은 실제 기기에서 실행되지 않습니다.

토큰이 신뢰할 수 있는 기기에서 제공되었는지 확인하려면 다음 코드 스니펫과 같이 deviceRecognitionVerdict가 예상대로 표시되는지 확인합니다.

Kotlin

val deviceIntegrity =
    JSONObject(payload).getJSONObject("deviceIntegrity")
val deviceRecognitionVerdict =
    if (deviceIntegrity.has("deviceRecognitionVerdict")) {
        deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    } else {
        ""
    }

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

Java

JSONObject deviceIntegrity =
    new JSONObject(payload).getJSONObject("deviceIntegrity");
String deviceRecognitionVerdict =
    deviceIntegrity.has("deviceRecognitionVerdict")
    ? deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    : "";

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

테스트 기기가 기기 무결성을 충족하는 데 문제가 있다면 공장 출고 시 ROM이 설치(예: 기기 재설정)되어 있고 부트로더가 잠겨 있는지 확인하세요. Play Console에서 Play Integrity API 테스트를 만들 수도 있습니다.

조건부 기기 라벨

앱이 PC용 Google Play 게임즈에 출시되는 경우 deviceRecognitionVerdict에는 다음 라벨도 포함될 수 있습니다.

MEETS_VIRTUAL_INTEGRITY
앱이 Google Play 서비스가 설치된 Android 지원 에뮬레이터에서 실행 중입니다. 에뮬레이터는 시스템 무결성 검사를 통과하고 핵심 Android 호환성 요구사항을 충족합니다.

기기 정보(선택사항)

무결성 확인 결과에서 추가 라벨 수신을 선택한 경우 deviceRecognitionVerdict에는 다음 추가 라벨이 포함될 수 있습니다.

MEETS_BASIC_INTEGRITY
앱이 기본 시스템 무결성 검사를 통과한 기기에서 실행됩니다. 기기는 Android 호환성 요구사항을 충족하지 못할 수 있고 Google Play 서비스 실행이 승인되지 않을 수도 있습니다. 예를 들어 기기가 인식할 수 없는 Android 버전을 실행하거나 잠금 해제된 부트로더를 보유하거나 제조업체의 인증을 받지 않았을 수 있습니다.
MEETS_STRONG_INTEGRITY
앱이 Google Play 서비스가 적용된 Android 지원 기기에서 실행되며 하드웨어 지원 부팅 무결성 증명과 같은 시스템 무결성을 강력히 보장합니다. 기기는 시스템 무결성 검사를 통과하고 Android 호환성 요구사항을 충족합니다.

단일 기기는 각 라벨의 기준이 충족되면 기기 무결성 확인 결과에 여러 기기 라벨을 반환합니다.

최근 기기 활동(베타)

또한 최근 기기 활동을 선택하여 지난 1시간 동안 특정 기기에서 앱이 무결성 토큰을 요청한 횟수를 파악할 수도 있습니다. 최근 기기 활동을 사용하여 활성 공격의 징후일 수 있는 예기치 않은 과도한 기기로부터 앱을 보호할 수 있습니다. 일반적인 기기에 설치된 앱이 매시간 무결성 토큰을 요청하는 횟수를 기준으로 최근의 각 기기 활동 수준을 얼마나 신뢰할지 결정할 수 있습니다.

recentDeviceActivity 수신을 선택하면 deviceIntegrity 필드에 다음 두 개의 값이 포함됩니다.

deviceIntegrity: {
  deviceRecognitionVerdict: "MEETS_DEVICE_INTEGRITY"
  recentDeviceActivity: "LEVEL_2"
}

recentDeviceActivity의 값은 다음 중 하나일 수 있습니다.

LEVEL_1
최저 수준: 앱이 지난 1시간 동안 이 기기에서 10개 이하의 무결성 토큰을 요청했습니다.
LEVEL_2
앱이 지난 1시간 동안 이 기기에 11~25개 사이의 무결성 토큰을 요청했습니다.
LEVEL_3
앱이 지난 1시간 동안 이 기기에 26~50개 사이의 무결성 토큰을 요청했습니다.
LEVEL_4
최고 수준: 앱이 지난 1시간 동안 이 기기에서 무결성 토큰을 50개 넘게 요청했습니다.
UNEVALUATED

최근 기기 활동이 평가되지 않았습니다. 이는 다음을 비롯하여 여러 가지 이유로 발생할 수 있습니다.

  • 기기를 충분히 신뢰할 수 없습니다.
  • 기기에 설치된 앱 버전을 Google Play에서 알 수 없습니다.
  • 기기의 기술 문제

등급 정의는 근사치이며 변경될 수 있습니다.

계정 세부정보 필드

accountDetails 필드에는 앱 라이선스 상태를 나타내는 단일 값 appLicensingVerdict가 포함됩니다.

accountDetails: {
  // This field can be LICENSED, UNLICENSED, or UNEVALUATED.
  appLicensingVerdict: "LICENSED"
}

appLicensingVerdict는 다음 값 중 하나를 보유할 수 있습니다.

LICENSED
사용자에게 앱 권한이 있습니다. 즉, 사용자가 Google Play에서 앱을 설치했거나 구매했습니다.
UNLICENSED
사용자에게 앱 권한이 없습니다. 예를 들어 사용자가 앱을 사이드로드한 경우 또는 Google Play에서 앱을 획득하지 않은 경우에 이러한 상황이 발생합니다. 사용자에게 GET_LICENSED 대화상자를 표시하여 이 문제를 해결할 수 있습니다.
UNEVALUATED

필요한 요구사항을 충족하지 못하여 라이선스 세부정보가 평가되지 않았습니다.

이는 다음을 비롯하여 여러 가지 이유로 발생할 수 있습니다.

  • 기기를 충분히 신뢰할 수 없습니다.
  • 기기에 설치된 앱 버전을 Google Play에서 알 수 없습니다.
  • 사용자가 Google Play에 로그인하지 않았습니다.

사용자에게 앱에 관한 앱 권한이 있는지 확인하려면 다음 코드 스니펫과 같이 appLicensingVerdict가 제대로 표시되는지 확인하세요.

Kotlin

val accountDetails = JSONObject(payload).getJSONObject("accountDetails")
val appLicensingVerdict = accountDetails.getString("appLicensingVerdict")

if (appLicensingVerdict == "LICENSED") {
    // Looks good!
}

Java

JSONObject accountDetails =
    new JSONObject(payload).getJSONObject("accountDetails");
String appLicensingVerdict = accountDetails.getString("appLicensingVerdict");

if (appLicensingVerdict.equals("LICENSED")) {
    // Looks good!
}

환경 세부정보 필드

Google Play Console에서 Play 프로텍트 확인 결과를 선택했다면 API 응답에 environmentDetails 필드가 포함됩니다. environmentDetails 필드에는 기기의 Google Play 프로텍트에 관한 정보를 제공하는 단일 값 playProtectVerdict가 포함됩니다.

environmentDetails: {
  playProtectVerdict: "NO_ISSUES"
}

playProtectVerdict의 값은 다음 중 하나일 수 있습니다.

NO_ISSUES
Play 프로텍트가 사용 설정되어 있으며 기기에서 앱 문제를 발견하지 못했습니다.
NO_DATA
Play 프로텍트가 사용 설정되어 있지만 아직 검사가 이루어지지 않았습니다. 기기 또는 Play 스토어 앱이 최근에 재설정되었을 수 있습니다.
POSSIBLE_RISK
Play 프로텍트가 사용 중지되어 있습니다.
MEDIUM_RISK
Play 프로텍트가 사용 설정되어 있으며 기기에 설치된 잠재적으로 위험한 앱을 발견했습니다.
HIGH_RISK
Play 프로텍트가 사용 설정되어 있으며 기기에 설치된 위험한 앱을 발견했습니다.
UNEVALUATED

Play 프로텍트 확인 결과가 평가되지 않았습니다.

이는 다음을 비롯하여 여러 가지 이유로 발생할 수 있습니다.

  • 기기를 충분히 신뢰할 수 없습니다.
  • 게임에만 적용: 사용자 계정이 LICENSED가 아닙니다.

Play 프로텍트 확인 결과를 사용하는 방법에 관한 안내

앱의 백엔드 서버는 위험 허용 범위에 기반한 확인 결과에 따라 조치 방법을 결정할 수 있습니다. 다음은 몇 가지 제안사항과 잠재적인 사용자 작업입니다.

NO_ISSUES
Play 프로텍트가 사용 설정되어 있으며 문제가 발견되지 않았으므로 사용자 작업이 필요하지 않습니다.
POSSIBLE_RISKNO_DATA
이러한 확인 결과를 수신하면 사용자에게 Play 프로텍트가 사용 설정되어 있고 검사를 실행했는지 확인하도록 요청합니다. NO_DATA는 드물게만 표시되어야 합니다.
MEDIUM_RISKHIGH_RISK
위험 허용 범위에 따라 사용자에게 Play 프로텍트를 실행하고 Play 프로텍트 경고에 조치를 취하도록 요청할 수 있습니다. 사용자가 이러한 요구사항을 충족할 수 없다면 서버 작업에서 차단할 수 있습니다.