App Device Integrity Plus
๐บ๐ธ English
Why?
What This Fork Fixes
The original app_device_integrity had a critical issue:
-
The challengeString (nonce) sent from Flutter was completely ignored.
-
The plugin always used a static Base64.encode(ByteArray(40)) value.
-
This produced the well-known "AAAAAAAAAAAA..." nonce in Play Integrity logs.
-
Real server-side verification was not possible because the nonce never matched.
Fixes in This Version
This fork fixes all of that.
-
Proper nonce passthrough from Flutter โ Native โ Play Integrity
-
Removed static dummy nonce (ByteArray(40))
-
Added proper MethodChannel argument handling
-
Updated API to accept challengeString exactly as given
-
Real attestation with server-side validation now works
-
README rewritten for clarity
-
Additional flow diagram for easier understanding
๐ How to Use
- Request nonce from your backend
Your server must generate a unique challenge per session.
final sessionId = await api.getNonce();
- Pass nonce into the plugin
final integrity = AppDeviceIntegrityPlus();
if (Platform.isAndroid) {
final token = await integrity.getAttestationServiceSupport(
challengeString: sessionId,
gcp: 523725941100,
);
} else {
final token = await integrity.getAttestationServiceSupport(
challengeString: sessionId,
);
}
- Send token to backend for validation
await api.verifyIntegrity(token);
๐ Attestation Flow (App โ API Server โ Google)
sequenceDiagram
participant APP
participant API as API Server
participant GOOGLE as Google Server (Play Integrity)
APP->>API: Request requestHash (based on request data)
API-->>APP: Generate & return requestHash
note right of APP: Play Integrity preparation (prepare phase)
APP->>GOOGLE: prepareIntegrityToken(cloudProjectNumber)
GOOGLE-->>APP: Return IntegrityTokenProvider
note right of APP: Standard request can now be executed
APP->>GOOGLE: provider.request(requestHash included)
GOOGLE-->>APP: Return Standard signed token (JWT)
APP->>API: Send token to server for verification
API->>GOOGLE: Validate & decrypt token
GOOGLE->>API: Return token payload (including requestHash)
note left of API: Compare requestHash with original
API-->>APP: OK (valid client) or Error (tampered/replay attack)
๐ References
๐ฐ๐ท ํ๊ตญ์ด
๐ง ์ ๋ง๋ค๊ฒ ๋์๋๊ฐ?
์๋ณธ app_device_integrity ํ๋ฌ๊ทธ์ธ์ ๋ฌธ์
- Integrity API์ ๊ธฐ์กด๋ฐฉ์ ์ ๊ณต(๋ ๊ฑฐ์)
- Flutter์์ ๋๊ธด challengeString(nonce)์ ์ ํ ์ฌ์ฉํ์ง ์์
- ๋ด๋ถ์์ ํญ์ ByteArray(40) โ Base64 ์ธ์ฝ๋ฉํ ๊ฐ ์ฌ์ฉ
- ๊ทธ๋์ Play Integrity ๋ก๊ทธ์ "AAAAAAAAAA..." nonce๋ง ์ถ๋ ฅ๋จ
- ์๋ฒ ๊ฒ์ฆ ์ nonce ๋ถ์ผ์น โ ์ ์์ ์ธ ๋ณด์ ๊ฒ์ฆ ๋ถ๊ฐ๋ฅ
โ ์ด ๋ฒ์ ์์ ์์ / ๊ฐ์ ๋ ๋ด์ฉ
-
Integrity API์ ํ์ค ๋ฐฉ์์ผ๋ก ๋ฆฌํํฐ๋ง
-
์๋ฒ์์ ๋ฐ์ nonce๋ฅผ ๊ทธ๋๋ก Play Integrity์ ์ ๋ฌ
-
๋ ์ด์ static dummy nonce ์ฌ์ฉํ์ง ์์
-
MethodChannel ํ๋ผ๋ฏธํฐ ์ฒ๋ฆฌ ์์
-
Android/iOS์์ ์ค์ nonce ๊ธฐ๋ฐ ํ ํฐ ์์ฑ ๊ฐ๋ฅ
-
README ์ ๋ฉด ์ฌ์์ฑ
-
ํ๋ก์ฐ ๋ค์ด์ด๊ทธ๋จ ์ถ๊ฐ
๐ ์ฌ์ฉ ๋ฐฉ๋ฒ
- ์๋ฒ์์ nonce ๋ฐ๊ธ
final sessionId = await api.getNonce();
- ํ๋ฌ๊ทธ์ธ์ nonce ์ ๋ฌ
final integrity = AppDeviceIntegrityPlus();
if (Platform.isAndroid) {
final token = await integrity.getAttestationServiceSupport(
challengeString: sessionId,
gcp: 523725941100,
);
} else {
final token = await integrity.getAttestationServiceSupport(
challengeString: sessionId,
);
}
- ํ ํฐ์ ์๋ฒ๋ก ์ ๋ฌํด ๊ฒ์ฆ
await api.verifyIntegrity(token);
๐ ์ ์ฒด ํ๋ก์ฐ (์ฑ โ ์๋ฒ โ Google)
sequenceDiagram
participant APP
participant API as API Server
participant GOOGLE as Google Server
APP->>API: requestHash ์์ฒญ (์์ฒญ ๋ฐ์ดํฐ ๊ธฐ๋ฐ)
API-->>APP: requestHash ์์ฑ & ๋ฐ๊ธ
note right of APP: ์ฑ ๋ด๋ถ์์ Play Integrity ์ค๋น(prepare)
APP->>GOOGLE: prepareIntegrityToken(cloudProjectNumber)
GOOGLE-->>APP: IntegrityTokenProvider ๋ฐํ
note right of APP: ์ด์ ํ์ค ์์ฒญ(request) ๊ฐ๋ฅ
APP->>GOOGLE: provider.request(requestHash ํฌํจ)
GOOGLE-->>APP: Standard signed token ๋ฐํ
APP->>API: token ์ ๋ฌ (๊ฒ์ฆ ์์ฒญ)
API->>GOOGLE: ๋ณตํธํ ๋ฐ ํ ํฐ ๊ฒ์ฆ
GOOGLE->>API: requestHash ๋ฐํ
API-->>APP: OK (์ ์) ๋๋ Error (์๋ณ์กฐ/์ฌ์ ์ก ๊ณต๊ฒฉ)
note left of API: requestHash ๋์กฐ