표준 API 요청

이 페이지에서는 Android 5.0(API 수준 21) 이상에서 지원되는 무결성 확인 결과에 관한 표준 API 요청을 실행하는 방법을 설명합니다. 앱에서 서버를 호출하여 상호작용이 진짜인지 확인할 때마다 무결성 확인 결과에 관한 표준 API 요청을 실행할 수 있습니다.

개요

Play Integrity API의 대략적인 설계를 보여주는 시퀀스 다이어그램

표준 요청은 다음 두 부분으로 구성됩니다.

  • 무결성 토큰 제공자 준비(일회성): 무결성 확인 결과를 얻기 훨씬 전에 Integrity API를 호출하여 무결성 토큰 제공자를 준비해야 합니다. 예를 들어 앱이 실행될 때 또는 무결성 확인 결과가 필요해지기 전에 백그라운드에서 이 작업을 실행할 수 있습니다.
  • 무결성 토큰 요청(주문형): 앱에서 진짜인지 확인하려는 서버 요청을 실행할 때마다 무결성 토큰을 요청하고 복호화 및 확인을 위해 앱의 백엔드 서버로 전송합니다. 그러면 백엔드 서버에서 어떻게 작업할지 결정할 수 있습니다.

다음과 같이 무결성 토큰 제공자를 준비합니다(일회성).

  1. 앱이 Google Cloud 프로젝트 번호를 사용하여 무결성 토큰 제공자를 호출합니다.
  2. 앱이 추가 증명 확인 호출을 위해 메모리에 무결성 토큰 제공자를 보관합니다.

다음과 같이 무결성 토큰을 요청합니다(주문형).

  1. 보호해야 하는 사용자 작업의 경우 앱은 실행할 요청의 해시(SHA256과 같은 적절한 해시 알고리즘 사용)를 계산합니다.
  2. 앱이 무결성 토큰을 요청하고 요청 해시를 전달합니다.
  3. 앱이 Play Integrity API에서 서명되고 암호화된 무결성 토큰을 수신합니다.
  4. 앱이 무결성 토큰을 앱의 백엔드에 전달합니다.
  5. 앱의 백엔드는 Google Play 서버로 토큰을 전송합니다. Google Play 서버는 확인 결과를 복호화하고 확인하여 결과를 앱의 백엔드로 반환합니다.
  6. 앱의 백엔드는 토큰 페이로드에 포함된 신호에 따라 진행 방법을 결정합니다.
  7. 앱의 백엔드는 결과를 앱에 전송합니다.

무결성 토큰 제공자 준비(일회성)

Google Play에서 무결성 확인 결과에 관한 표준 요청을 하기 전에 무결성 토큰 제공자를 준비해야 합니다. 이렇게 하면 무결성 확인 결과 요청을 할 때 중요한 경로의 지연 시간을 줄이기 위해 Google Play에서 기기의 부분 증명 정보를 스마트하게 캐시할 수 있습니다. 토큰 제공자를 다시 준비하면 리소스 의존도가 높은 무결성 검사를 덜 반복해 다음에 요청하는 무결성 확인 결과가 더 최신 상태로 유지됩니다.

다음과 같이 무결성 토큰 제공자를 준비할 수 있습니다.

  • 앱이 실행될 때(콜드 스타트 시). 토큰 제공자 준비는 비동기식이므로 시작 시간에 영향을 주지 않습니다. 이 옵션은 사용자가 로그인하거나 플레이어가 게임에 참여할 때 등 앱이 실행된 직후 무결성 확인 결과를 요청하려는 경우 적합합니다.
  • 앱이 열릴 때(웜 스타트 시). 그러나 각 앱 인스턴스는 무결성 토큰을 분당 최대 5회까지만 준비할 수 있습니다.
  • 무결성 확인 결과 요청 전에 미리 토큰을 준비하려는 경우 백그라운드에서 언제든지 준비

무결성 토큰 제공자를 준비하려면 다음 단계를 따르세요.

  1. 다음 예와 같이 StandardIntegrityManager를 만듭니다.
  2. PrepareIntegrityTokenRequest를 구성하여 setCloudProjectNumber() 메서드를 통해 Google Cloud 프로젝트 번호를 제공합니다.
  3. 관리자를 사용하여 prepareIntegrityToken()을 호출해 PrepareIntegrityTokenRequest를 제공합니다.

Java

import com.google.android.gms.tasks.Task;

// Create an instance of a manager.
StandardIntegrityManager standardIntegrityManager =
    IntegrityManagerFactory.createStandard(applicationContext);

StandardIntegrityTokenProvider integrityTokenProvider;
long cloudProjectNumber = ...;

// Prepare integrity token. Can be called once in a while to keep internal
// state fresh.
standardIntegrityManager.prepareIntegrityToken(
    PrepareIntegrityTokenRequest.builder()
        .setCloudProjectNumber(cloudProjectNumber)
        .build())
    .addOnSuccessListener(tokenProvider -> {
        integrityTokenProvider = tokenProvider;
    })
    .addOnFailureListener(exception -> handleError(exception));

Unity

IEnumerator PrepareIntegrityTokenCoroutine() {
    long cloudProjectNumber = ...;

    // Create an instance of a standard integrity manager.
    var standardIntegrityManager = new StandardIntegrityManager();

    // Request the token provider.
    var integrityTokenProviderOperation =
      standardIntegrityManager.PrepareIntegrityToken(
        new PrepareIntegrityTokenRequest(cloudProjectNumber));

    // Wait for PlayAsyncOperation to complete.
    yield return integrityTokenProviderOperation;

    // Check the resulting error code.
    if (integrityTokenProviderOperation.Error != StandardIntegrityErrorCode.NoError)
    {
        AppendStatusLog("StandardIntegrityAsyncOperation failed with error: " +
                integrityTokenProviderOperation.Error);
        yield break;
    }

    // Get the response.
    var integrityTokenProvider = integrityTokenProviderOperation.GetResult();
}

네이티브

/// Initialize StandardIntegrityManager
StandardIntegrityManager_init(/* app's java vm */, /* an android context */);
/// Create a PrepareIntegrityTokenRequest opaque object.
int64_t cloudProjectNumber = ...;
PrepareIntegrityTokenRequest* tokenProviderRequest;
PrepareIntegrityTokenRequest_create(&tokenProviderRequest);
PrepareIntegrityTokenRequest_setCloudProjectNumber(tokenProviderRequest, cloudProjectNumber);

/// Prepare a StandardIntegrityTokenProvider opaque type pointer and call
/// StandardIntegrityManager_prepareIntegrityToken().
StandardIntegrityTokenProvider* tokenProvider;
StandardIntegrityErrorCode error_code =
        StandardIntegrityManager_prepareIntegrityToken(tokenProviderRequest, &tokenProvider);

/// ...
/// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR
if (error_code != STANDARD_INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.

IntegrityResponseStatus token_provider_status;

/// Check for error codes.
StandardIntegrityErrorCode error_code =
        StandardIntegrityTokenProvider_getStatus(tokenProvider, &token_provider_status);
if (error_code == STANDARD_INTEGRITY_NO_ERROR
    && token_provider_status == INTEGRITY_RESPONSE_COMPLETED)
{
    /// continue to request token from the token provider
}
/// ...
/// Remember to free up resources.
PrepareIntegrityTokenRequest_destroy(tokenProviderRequest);

요청 조작 방지(권장)

Play Integrity API로 앱에서 사용자 작업을 확인할 때 requestHash 필드를 활용하여 조작 공격을 완화할 수 있습니다. 예를 들어 게임에서 플레이어의 점수를 게임의 백엔드 서버에 보고하려고 할 때 서버는 프록시 서버에서 이 점수가 조작되지 않았는지 확인하려고 합니다. Play Integrity API는 서명된 무결성 응답 내에서 requestHash 필드에 설정한 값을 반환합니다. requestHash가 없으면 무결성 토큰은 기기에만 바인딩되고 특정 요청에는 바인딩되지 않으므로 공격이 발생할 수 있습니다. 다음 안내에서는 requestHash 필드를 효과적으로 사용하는 방법을 설명합니다.

무결성 확인 결과를 요청하는 경우:

  • 발생하는 사용자 작업 또는 서버 요청에서 모든 관련 요청 매개변수(예: 안정적인 요청 직렬화의 SHA256)의 다이제스트를 계산합니다. requestHash 필드에 설정된 값의 최대 길이는 500바이트입니다. 확인 또는 보호하는 작업에 중요하거나 이러한 작업과 관련된 앱 요청 데이터를 requestHash에 포함합니다. requestHash 필드는 무결성 토큰에 그대로 포함되어 있으므로 긴 값으로 인해 요청 크기가 늘어날 수도 있습니다.
  • Play Integrity API에 requestHash 필드로 다이제스트를 제공하고 무결성 토큰을 가져옵니다.

무결성 확인 결과를 수신하는 경우:

  • 무결성 토큰을 디코딩하고 requestHash 필드를 추출합니다.
  • 앱과 동일한 방식으로 요청 다이제스트를 계산합니다(예: 안정적인 요청 직렬화의 SHA256).
  • 앱 측 다이제스트와 서버 측 다이제스트를 비교합니다. 일치하지 않으면 요청을 신뢰할 수 없습니다.

무결성 확인 결과 요청(주문형)

무결성 토큰 제공자가 준비되면 Google Play에서 무결성 확인 결과 요청을 시작할 수 있습니다. 그러려면 다음 단계를 완료하세요.

  1. 위와 같이 StandardIntegrityTokenProvider를 가져옵니다.
  2. StandardIntegrityTokenRequest를 구성하여 setRequestHash 메서드를 통해 보호하려는 사용자 작업의 요청 해시를 제공합니다.
  3. 무결성 토큰 제공자를 사용하여 request()를 호출해 StandardIntegrityTokenRequest를 제공합니다.

Java

import com.google.android.gms.tasks.Task;

StandardIntegrityTokenProvider integrityTokenProvider;

// See above how to prepare integrityTokenProvider.

// Request integrity token by providing a user action request hash. Can be called
// several times for different user actions.
String requestHash = "2cp24z...";
Task<StandardIntegrityToken> integrityTokenResponse =
    integrityTokenProvider.request(
        StandardIntegrityTokenRequest.builder()
            .setRequestHash(requestHash)
            .build());
integrityTokenResponse
    .addOnSuccessListener(response -> sendToServer(response.token()))
    .addOnFailureListener(exception -> handleError(exception));

Unity

IEnumerator RequestIntegrityTokenCoroutine() {
    StandardIntegrityTokenProvider integrityTokenProvider;

    // See above how to prepare integrityTokenProvider.

    // Request integrity token by providing a user action request hash. Can be called
    // several times for different user actions.
    String requestHash = "2cp24z...";
    var integrityTokenOperation = integrityTokenProvider.Request(
      new StandardIntegrityTokenRequest(requestHash)
    );

    // Wait for PlayAsyncOperation to complete.
    yield return integrityTokenOperation;

    // Check the resulting error code.
    if (integrityTokenOperation.Error != StandardIntegrityErrorCode.NoError)
    {
        AppendStatusLog("StandardIntegrityAsyncOperation failed with error: " +
                integrityTokenOperation.Error);
        yield break;
    }

    // Get the response.
    var integrityToken = integrityTokenOperation.GetResult();
}

네이티브

/// Create a StandardIntegrityTokenRequest opaque object.
const char* requestHash = ...;
StandardIntegrityTokenRequest* tokenRequest;
StandardIntegrityTokenRequest_create(&tokenRequest);
StandardIntegrityTokenRequest_setRequestHash(tokenRequest, requestHash);

/// Prepare a StandardIntegrityToken opaque type pointer and call
/// StandardIntegrityTokenProvider_request(). Can be called several times for
/// different user actions. See above how to prepare token provider.
StandardIntegrityToken* token;
StandardIntegrityErrorCode error_code =
        StandardIntegrityTokenProvider_request(tokenProvider, tokenRequest, &token);

/// ...
/// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR
if (error_code != STANDARD_INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.

IntegrityResponseStatus token_status;

/// Check for error codes.
StandardIntegrityErrorCode error_code =
        StandardIntegrityToken_getStatus(token, &token_status);
if (error_code == STANDARD_INTEGRITY_NO_ERROR
    && token_status == INTEGRITY_RESPONSE_COMPLETED)
{
    const char* integrityToken = StandardIntegrityToken_getToken(token);
}
/// ...
/// Remember to free up resources.
StandardIntegrityTokenRequest_destroy(tokenRequest);
StandardIntegrityToken_destroy(token);
StandardIntegrityTokenProvider_destroy(tokenProvider);
StandardIntegrityManager_destroy();

무결성 확인 결과 복호화 및 확인

무결성 확인 결과를 요청하면 Play Integrity API가 암호화된 응답 토큰을 제공합니다. 기기 무결성 확인 결과를 가져오려면 Google 서버에서 무결성 토큰을 복호화해야 합니다. 이렇게 하려면 다음 단계를 완료하세요.

  1. 앱에 연결된 Google Cloud 프로젝트 내에서 서비스 계정을 만듭니다. 계정을 만드는 과정에서 서비스 계정 사용자서비스 사용량 소비자 역할을 서비스 계정에 부여해야 합니다.
  2. 앱 서버에서 playintegrity 범위를 사용하여 서비스 계정 사용자 인증 정보로부터 액세스 토큰을 가져오고 다음과 같이 요청합니다.

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. JSON 응답을 읽습니다.

결과 페이로드는 무결성 확인 결과가 포함된 일반 텍스트 토큰입니다.

자동 재생 보호

재생 공격을 완화하기 위해 Google Play는 각 무결성 토큰을 여러 번 재사용할 수 없도록 자동으로 보장합니다. 동일한 토큰을 반복적으로 복호화하려고 하면 빈 결과가 나옵니다.