AppIntegrity

一个库,可在 Android 上访问 Google Play Integrity API,并在 iOS 上访问 Apple 的 App Attest 服务。

Android
iOS
Included in Expo Go

@expo/app-integrity 提供 API,帮助确保只有在真实设备上运行的合法应用安装才能访问你的后端资源。它在 Android 上使用 Google 的 Play 完整性 API,在 iOS 上使用 Apple 的 App Attest 服务 来验证应用的真实性,从而帮助防止未经授权的客户端、修改过的应用或自动化脚本向你的服务器发出请求。

一般来说,@expo/app-integrity 可以帮助你的服务器区分以下内容:

🌐 Generally, @expo/app-integrity helps your server tell the difference between:

  • 你的 真实应用真实设备 上运行
  • 其他(修改后的应用、脚本、模拟器)

它通过使用平台推荐的应用认证服务来实现这一点。

🌐 It does this by using the platform-recommended app attestation services.

安装

🌐 Installation

Terminal
npx expo install @expo/app-integrity

If you are installing this in an existing React Native app, make sure to install expo in your project.

Android 上的使用

🌐 Usage on Android

@expo/app-integrity 使用 Play Integrity 的标准请求流程进行完整性检查。

配置

🌐 Configuration

请参阅Play 完整性设置指南以获取在你的应用中启用完整性 API 的说明。

🌐 Refer to the Play Integrity setup guide for instructions to enable integrity APIs in your app.

准备完整性令牌提供程序(一次性)

🌐 Prepare the integrity token provider (one time)

在进行完整性检查请求之前,你需要先准备完整性令牌提供者。你可以在应用启动时或在需要完整性检查之前的后台进行此操作。

🌐 You need to prepare the integrity token provider before you make integrity check requests. You can do this when your app launches or in the background before the integrity check is needed.

import * as AppIntegrity from '@expo/app-integrity'; const cloudProjectNumber = 'your-cloud-project-number'; await AppIntegrity.prepareIntegrityTokenProviderAsync(cloudProjectNumber);

请求完整性令牌(按需)

🌐 Request an integrity token (on demand)

每当你的应用发出你想要验证其真实性的服务器请求时,你需要请求一个完整性令牌并将其发送到应用的后端服务器进行解密和验证。然后,你的后端服务器可以决定如何处理。

🌐 Whenever your app makes a server request that you want to check is genuine, you request an integrity token and send it to your app's backend server for decryption and verification. Then, your backend server can determine how to act.

const requestHash = '2cp24z...'; const result = await AppIntegrity.requestIntegrityCheckAsync(requestHash);

在调用 requestIntegrityCheckAsync 之前,确保 prepareIntegrityTokenProviderAsync 已成功调用。

🌐 Before calling requestIntegrityCheckAsync, ensure that prepareIntegrityTokenProviderAsync was called successfully.

在此示例中,requestHash 是针对正在验证的特定用户操作的唯一哈希。你可以用不同用户操作的不同哈希多次调用 requestIntegrityCheckAsync

🌐 In this example, requestHash is a hash unique to the specific user action being verified. You can call requestIntegrityCheckAsync multiple times with different hashes for different user actions.

成功后,将结果发送到你的服务器进行验证。

🌐 On success, send the result to your server for verification.

注意:如果你的应用长时间使用同一个令牌提供者,该令牌提供者可能会过期,从而在下一次令牌请求时导致 ERR_APP_INTEGRITY_PROVIDER_INVALID 错误。你应通过再次调用 prepareIntegrityTokenProviderAsync 来请求新的提供者,以处理此错误。

解密并验证完整性判定

🌐 Decrypt and verify the integrity verdict

请参考 Play Integrity 指南 在你的服务器中验证完整性令牌。

🌐 Refer Play Integrity's guide to verify the integrity token in your server.

其他资源

🌐 Additional resources

  • Google Pay 完整性文档:请参考 Google 官方指南,了解支持 @expo/app-integrity 的 API 和验证流程。
  • Play 完整性标准请求流程:本页面描述了如何为完整性裁决发出标准 API 请求,该功能在 Android 5.0(API 级别 21)或更高版本上受支持。每当你的应用发起服务器调用以检查交互是否真实时,你都可以发出完整性裁决的标准 API 请求。
  • 关于完整性裁决:完整性裁决传达有关设备、应用和账户有效性的信息。你的应用服务器可以使用解密且经过验证的裁决结果负载来确定如何最好地处理应用中的特定操作或请求。
  • 处理错误代码:如果你的应用发起 Play Integrity API 请求时调用失败,你的应用将收到一个错误代码。这些错误可能由多种原因引起,例如网络连接不稳定等环境问题、API 集成存在问题,或恶意活动和主动攻击。

iOS 上的使用

🌐 Usage on iOS

配置

🌐 Configuration

在 Xcode 中,进入 Signing & Capabilities,点击 + Capability,添加 App Attest。Xcode 会自动向你的应用添加所需的权限。

🌐 In Xcode, go to Signing & Capabilities, click + Capability, add App Attest. Xcode will automatically add the required entitlement to your app.

注意:要使用 App Attest 服务,你的应用必须拥有在 Apple 开发者网站上注册的应用 ID。

有关服务器上的验证逻辑,请参阅 验证连接到你服务器的应用

🌐 For verification logic on your server, see Validating apps that connect to your server.

检查设备是否支持应用认证

🌐 Check if the device supports app attestation

并非所有设备都可以使用 App Attest 服务,因此在访问该服务之前,让你的应用运行兼容性检查非常重要。如果用户的应用未通过兼容性检查,请优雅地跳过该服务。你可以通过读取 isSupported 属性来检查其可用性。

🌐 Not all devices can use the App Attest service, so it's important to have your app run a compatibility check before accessing the service. If the user's app doesn't pass the compatibility check, gracefully bypass the service. You can check for availability by reading the isSupported property.

import * as AppIntegrity from '@expo/app-integrity'; if (AppIntegrity.isSupported) { // Perform key generation and attestation. } // Continue with your server API access.

注意:iOS 模拟器不支持 App Attest。

信息 大多数应用扩展不支持 App Attest。通常,在这些扩展中执行代码时,即使 isSupported 方法属性为 true,也应跳过密钥生成和证明。唯一支持 App Attest 的应用扩展是运行在 watchOS 9 或更高版本的 watchOS 扩展。对于这些扩展,你可以使用 isSupported 的结果来指示你的 WatchKit 扩展是否跳过了证明。

创建密钥配对

🌐 Create a key pair

对于运行你应用的每个设备上的每个用户账户,通过调用 generateKey 方法生成一个唯一的、基于硬件的加密密钥对。

🌐 For each user account on each device running your app, generate a unique, hardware-based, cryptographic key pair by calling the generateKey method.

const keyId = await AppIntegrity.generateKeyAsync();

成功时,该方法会返回一个密钥标识符(keyId),你可以在之后用它来访问密钥。请将标识符记录在持久化存储中,因为没有标识符就无法使用密钥,也无法在以后获取标识符。设备会自动将相关的私钥存储在安全隔区(Secure Enclave)中,App Attest 服务可以从中使用私钥创建签名,但没有任何进程可以直接读取或修改它,从而确保其安全性。

🌐 On success, the method returns a key identifier (keyId) that you use later to access the key. Record the identifier in persistent storage because there's no way to use the key without the identifier and no way to get the identifier later. The device automatically stores the associated private key in the Secure Enclave, from where the App Attest service can use it to create signatures, but from where no process can ever directly read or modify it, ensuring its security.

信息 如果你在 App Clip 中创建密钥对,请在对应的完整应用中使用相同的密钥对。为支持此操作,请确保将标识符存储在完整应用可以访问的共享容器中。请参阅 Expo 关于使用 expo-sqlite 在应用/扩展之间共享数据库的指南,或使用 React Native MMKV 的 App Groups / 扩展 共享存储在两个目标之间持久保存该标识符。

不要在设备上的多个用户之间重复使用同一个密钥,因为这会削弱安全保护。特别是,这会使检测使用单个被攻破设备为多个远程用户提供服务(这些用户运行的是被篡改的应用版本)的攻击变得困难。欲了解更多信息,请参阅[评估欺诈风险](https://developer.apple.com/documentation/devicecheck/assessing-fraud-risk)。

🌐 Don't reuse a key among multiple users on a device because this weakens security protections. In particular, it becomes hard to detect an attack that uses a single compromised device to serve multiple remote users running a compromised version of your app. For more information, see Assessing fraud risk.

从你的服务器获取质询

🌐 Get a challenge from your server

向你的服务器请求一个独特的一次性挑战。这一挑战将嵌入下面的证明步骤中,确保攻击者无法重复使用。挑战长度应至少为16字节,以提供足够的熵,使猜测其值变得不可能。

🌐 Request a unique, one-time challenge from your server. This challenge will be embedded in the attestation step below, ensuring it can't be reused by an attacker. The challenge should be at least 16 bytes long to provide enough entropy so that guessing it is infeasible.

验证密钥对是否有效

🌐 Certify the key pairs as valid

keyId 以及你在前面步骤中服务器创建的挑战通过 attestKey 方法传送,如下所示:

🌐 Pass the keyId alongwith the challenge from your server created in the previous steps in attestKey method as shown below:

const attestationObject = await AppIntegrity.attestKeyAsync(keyId, challenge);

成功后,将收到的 attestationObjectkeyId 发送到你的服务器以进行验证。

🌐 On success, send the received attestationObject and the keyId to your server for verification.

如果该方法返回 ERR_APP_INTEGRITY_SERVER_UNAVAILABLE 错误,请稍后使用相同的密钥再次尝试证明。对于任何其他错误,请丢弃密钥标识符,并在想要再次尝试时创建一个新密钥。

🌐 If the method returns ERR_APP_INTEGRITY_SERVER_UNAVAILABLE error, try attestation again later with the same key. For any other error, discard the key identifier and create a new key when you want to try again.

信息 如果你的应用已经拥有数百万日活跃用户,并且你想开始调用 attestKey 方法从你的应用发起验证,请查阅 准备使用应用验证服务 以获取有关如何安全地逐步启用用户的指南。

如果服务器能够成功验证声明对象,则认为应用实例是有效的。在这种情况下,请确保在应用中持久存储密钥标识符,而不是声明对象,以便将来签署服务器请求。

🌐 Your server deems the app instance to be valid if it can successfully verify the attestation object. In this case, be sure to persistently store the key identifier — not the attestation object — in your app to sign server requests in the future.

对敏感请求生成断言

🌐 Generate assertions on sensitive requests

在成功验证密钥的认证后,你的服务器可以要求应用在任何或所有未来的服务器请求中证明其合法性。应用通过对请求进行签名来实现这一点。在应用中,从服务器获取一个独特的一次性挑战。这里使用挑战,就像认证一样,以防止重放攻击。

🌐 After successfully verifying a key's attestation, your server can require the app to assert its legitimacy for any or all future server requests. The app does this by signing the request. In the app, obtain a unique, one-time challenge from the server. You use a challenge here, like for attestation, to avoid replay attacks.

const challenge = 'A string from your server'; const request = { action: 'getGameLevel', levelId: '1234', challenge: challenge, }; const assertion = await AppIntegrity.generateAssertionAsync(keyId, JSON.stringify(request));

成功后,将断言对象以及客户端数据传递给服务器。如果断言对象验证失败,由你来决定如何处理该请求。

🌐 On success, pass the assertion object, along with the client data, to the server. If the assertion object fails verification, it's your responsibility to decide how to handle the request.

对于使用密钥可以进行的声明数量没有限制。不过,通常你会将声明保留用于在应用生命周期中的敏感时刻发出的请求,例如当应用下载高级内容时。

🌐 There's no restriction on the number of assertions that you can make with a key. Nevertheless, you typically reserve assertions for requests made at sensitive moments in your app's life cycle, like when the app downloads premium content.

重新安装后重新开始

🌐 Start over on reinstallation

你生成的密钥在常规应用更新中仍然有效,但在应用重新安装、设备迁移或从备份恢复设备时不会保留。在这些情况下,你需要从头开始重新生成一个新密钥。尽量将新密钥的生成仅限制在这些事件发生时,或在添加新用户时。保持设备上密钥的数量较少,有助于检测某些类型的欺诈行为。

🌐 The keys that you generate remain valid through regular app updates, but don't survive app reinstallation, device migration, or restoration of a device from a backup. In these cases, you need to start the process from the beginning and generate a new key. Try to limit new key generation to only these events, or to the addition of new users. Keeping the key count low on a device helps when trying to detect certain kinds of fraud.

其他资源

🌐 Additional resources

应用接口

🌐 API

import * as AppIntegrity from '@expo/app-integrity';

Constants

AppIntegrity.isSupported

iOS

Type: boolean

A boolean value that indicates whether a particular device provides the App Attest service. Not all device types support the App Attest service, so check for support before using the service.

Methods

AppIntegrity.attestKeyAsync(keyId, challenge)

iOS
ParameterTypeDescription
keyIdstring

The identifier you received by calling the generateKey function.

challengestring

A challenge string from your server.


Asks Apple to attest to the validity of a generated cryptographic key.

Returns:
Promise<string>

A Promise that is fulfilled with a string that contains the attestation data. A statement from Apple about the validity of the key associated with keyId. Send this to your server for processing.

AppIntegrity.generateAssertionAsync(keyId, challenge)

iOS
ParameterTypeDescription
keyIdstring

The identifier you received by calling the generateKey function.

challengestring

A string to be signed with the attested private key.


Creates a block of data that demonstrates the legitimacy of an instance of your app running on a device.

Returns:
Promise<string>

A Promise that is fulfilled with a string that contains the assertion object. A data structure that you send to your server for processing.

AppIntegrity.generateHardwareAttestedKeyAsync(keyAlias, challenge)

Android
ParameterTypeDescription
keyAliasstring

A unique identifier for the key.

challengestring

A challenge string from your server.


Generates a hardware-attested key pair in the Android Keystore. This key can be used for attestation on GrapheneOS and other secure Android distributions.

Returns:
Promise<void>

A Promise that resolves when the key is generated successfully.

AppIntegrity.generateKeyAsync()

iOS

Creates a new cryptographic key for use with the App Attest service.

Returns:
Promise<string>

A Promise that is fulfilled with a string that contains the key identifier. The key itself is stored securely in the Secure Enclave.

AppIntegrity.getAttestationCertificateChainAsync(keyAlias)

Android
ParameterTypeDescription
keyAliasstring

The identifier of the key to get certificates for.


Retrieves the attestation certificate chain for a hardware-attested key. The certificate chain can be validated on your server to verify device integrity.

Returns:
Promise<string[]>

A Promise that is fulfilled with an array of base64-encoded X.509 certificates.

AppIntegrity.isHardwareAttestationSupportedAsync()

Android

Checks if hardware attestation is supported on this device.

Returns:
Promise<boolean>

A Promise that is fulfilled with a boolean indicating support.

AppIntegrity.prepareIntegrityTokenProviderAsync(cloudProjectNumber)

Android
ParameterTypeDescription
cloudProjectNumberstring

The cloud project number.


Prepares the integrity token provider for the given cloud project number.

Returns:
Promise<void>

A Promise that is fulfilled if the integrity token provider is prepared successfully.

AppIntegrity.requestIntegrityCheckAsync(requestHash)

Android
ParameterTypeDescription
requestHashstring

A string representing the request hash.


Requests an integrity verdict for the given request hash from Google Play.

Returns:
Promise<string>

A Promise that is fulfilled with a string that contains the integrity check result.