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

  1. Request nonce from your backend

Your server must generate a unique challenge per session.

final sessionId = await api.getNonce();
  1. 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,
  );
}
  1. 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 ์ „๋ฉด ์žฌ์ž‘์„ฑ

  • ํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ถ”๊ฐ€

๐Ÿš€ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•

  1. ์„œ๋ฒ„์—์„œ nonce ๋ฐœ๊ธ‰
final sessionId = await api.getNonce();
  1. ํ”Œ๋Ÿฌ๊ทธ์ธ์— nonce ์ „๋‹ฌ
final integrity = AppDeviceIntegrityPlus();

if (Platform.isAndroid) {
  final token = await integrity.getAttestationServiceSupport(
    challengeString: sessionId,
    gcp: 523725941100,
  );
} else {
  final token = await integrity.getAttestationServiceSupport(
    challengeString: sessionId,
  );
}
  1. ํ† ํฐ์„ ์„œ๋ฒ„๋กœ ์ „๋‹ฌํ•ด ๊ฒ€์ฆ
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 ๋Œ€์กฐ

๐Ÿ“š ์ฐธ๊ณ