Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 1.2.0

* Added Swift Package Manager (SPM) support for iOS
* Implemented `generateAssertion` API for iOS App Attest
* Added automatic Base64URL challenge detection and decoding
* Enhanced error handling with detailed error codes
* Migrated example project from CocoaPods to SPM
* Updated Swift code with proper completion handlers
* Improved documentation with comprehensive API examples
* Added assertion generation for validating previously attested devices

## 1.1.0

* Updating Swift Implementation - contrib. [@Nebojsa92](https://github.com/Nebojsa92)
Expand Down
268 changes: 232 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,260 @@
<hr>
<hr>

This plugin was created to make your app attestation more easy. It uses the Native Attestation Providers from Apple and Google, App Attest and Play Integrity respectively, to generate tokens to be decrypted by your Server to check if your app is being accessed by a reliable device.
This plugin simplifies app attestation by using Apple's App Attest and Google's Play Integrity to generate tokens for your server to verify device integrity. It ensures your app is being accessed by a reliable, non-compromised device.

## How to Use It
## Features

- **iOS App Attest Integration**: Generate attestation tokens and assertions using Apple's App Attest framework
- **Android Play Integrity**: Support for Google Play Integrity API
- **Base64URL Challenge Support**: Automatically handles both plain text and Base64URL encoded challenges
- **Assertion Generation**: Validate previously attested devices with assertion API (iOS only)
- **Swift Package Manager Support**: Modern iOS dependency management (SPM compatible)

## Platform Support

| Platform | Attestation | Assertion | Min Version |
|----------|------------|-----------|-------------|
| iOS | ✓ | ✓ | iOS 16.0+ |
| Android | ✓ | - | API 19+ |

To use this plugin correctly, you need to contemplate these two steps:
## Installation

Provide a Session UUID, in iOS Case<br>
Provide your GCP Project ID, in Android Case<br>
Add to your `pubspec.yaml`:
```yaml
dependencies:
app_device_integrity: ^1.2.0
```

## How to Use It

### Initial Setup

**iOS:**<br>
The Session UUID is the challenge created by your server for App Attest to issue the token requested from the device to be "marked".<br>
Basically your server sends the challenge to Apple to ensure that the token is marked with it, attested by the service with it so that when the server receives the token, it was actually sent by the device with that session UUID sent by the server before.
The challenge is a unique session identifier created by your server for App Attest. This can be either:
- Plain text string (will be converted to UTF-8 bytes and hashed)
- Base64URL encoded string (will be automatically detected and decoded)

The plugin automatically handles both formats. Your server sends the challenge to Apple to ensure the attestation token is cryptographically bound to it. This prevents replay attacks and ensures the token was generated for this specific session.

**Android:**<br>
Providing the GCP Project ID links your app with your development and deployment environment. Before you implement it in your project, you need to follow the steps provided by the following doc:<br>
https://developer.android.com/google/play/integrity/setup?set-google-console#set-google-console
You need to provide your GCP (Google Cloud Platform) Project ID to link your app with your development environment.

Before implementing in your project, follow these steps:
1. Visit [Google Play Integrity Setup](https://developer.android.com/google/play/integrity/setup?set-google-console#set-google-console)
2. Link your GCP Project to your app in Google Play Console
3. Find your Project ID next to your project name in the GCP Console
4. It's recommended to store the Project ID as an environment variable

To be more "precise", when you link you GCP Project to your app in Google Play Console, the project ID is right beside of your project name. That's the information you need.<br>
It's recommended to create environmental variable with the project ID to maintain your app, and project, integrity.
## Implementation

### How To Implement
After you import the plugin to your project, implement the token generation using the following steps:
### 1. Generate Initial Attestation Token

Use this when first attesting a device:

```dart
import 'package:app_device_integrity/app_device_integrity.dart';

final deviceIntegrity = AppDeviceIntegrity();

// Instance the way you prefer
final _appAttestationPlugin = AppDeviceIntegrity();
// iOS Example
Future<void> attestDeviceIOS() async {
try {
// Challenge from your server (can be plain text or Base64URL)
final sessionId = '550e8400-e29b-41d4-a716-446655440000';

final tokenJson = await deviceIntegrity.getAttestationServiceSupport(
challengeString: sessionId,
);

// Parse the JSON response
final token = jsonDecode(tokenJson!);
final attestationString = token['attestationString'];
final keyID = token['keyID'];

// Send both to your server for verification
await sendToServer(attestationString, keyID);
} catch (e) {
print('Attestation failed: $e');
}
}

// You need to provide
// An UUID generated by your server (you can customize the rules to generate it)
// In case the platform is Android, the GCP Project ID needs to be informed.
// For a more practical implementation, check the plugin example.
// Android Example
Future<void> attestDeviceAndroid() async {
try {
final sessionId = '550e8400-e29b-41d4-a716-446655440000';
const gcpProjectId = 123456789; // Your GCP Project ID

final token = await deviceIntegrity.getAttestationServiceSupport(
challengeString: sessionId,
gcp: gcpProjectId,
);

// Send to your server for verification
await sendToServer(token);
} catch (e) {
print('Attestation failed: $e');
}
}
```

String sessionId = '550e8400-e29b-41d4-a716-446655440000';
int gpc = 0000000000; // YOUR GCP PROJECT ID IN ANDROID
### 2. Generate Assertion for Previously Attested Device (iOS Only)

if (Platform.isAndroid) {
After initial attestation, use assertions to validate the device without re-attesting:

```dart
Future<void> generateAssertion() async {
try {
final deviceIntegrity = AppDeviceIntegrity();

// Use the keyID from initial attestation
final keyID = 'saved_key_id_from_initial_attestation';

// Client data to sign (can be plain text or Base64URL)
// Should be at least 16 bytes when hashed with SHA256
final clientData = 'unique_session_data_${DateTime.now().millisecondsSinceEpoch}';

final assertionString = await deviceIntegrity.generateAssertion(
keyID: keyID,
clientData: clientData,
);

// Send assertion to your server for verification
await verifyAssertion(assertionString, clientData);
} on PlatformException catch (e) {
if (e.code == 'UNSUPPORTED_PLATFORM') {
print('Assertions are only supported on iOS');
} else {
print('Assertion generation failed: ${e.message}');
}
}
}
```

### Platform-Specific Implementation

```dart
import 'dart:io';

Future<void> attestDevice() async {
final deviceIntegrity = AppDeviceIntegrity();
final sessionId = await getSessionIdFromServer();

if (Platform.isAndroid) {
const gcpProjectId = 123456789;
final token = await deviceIntegrity.getAttestationServiceSupport(
challengeString: sessionId,
gcp: gcpProjectId,
);
return sendToServer(token);
}

tokenReceived = await _appAttestationPlugin
.getAttestationServiceSupport(challengeString: sessionId, gcp: gpc);
if (Platform.isIOS) {
final tokenJson = await deviceIntegrity.getAttestationServiceSupport(
challengeString: sessionId,
);
final token = jsonDecode(tokenJson!);
return sendToServer(token['attestationString'], token['keyID']);
}
}
```

return;

}
## Server-Side Verification

I provide an open-source server implementation for token verification:
[App Device Integrity Server](https://github.com/Erluan/app_device_integrity_server)

The server:
- Detects platform (iOS/Android) from the token
- Verifies tokens with Apple/Google services
- Provides risk assessment and device integrity information
- Helps make security decisions based on device trustworthiness

Feel free to fork, clone, and customize it according to your business requirements.

tokenReceived = await _appAttestationPlugin
.getAttestationServiceSupport(challengeString: sessionId);

return;
## API Reference

### `getAttestationServiceSupport`

Generates an attestation token for the device.

**Parameters:**
- `challengeString` (required): Session identifier from your server (plain text or Base64URL)
- `gcp` (Android only): Google Cloud Platform Project ID

**Returns:**
- iOS: JSON string with `attestationString` and `keyID`
- Android: Attestation token string

**Example:**
```dart
final token = await deviceIntegrity.getAttestationServiceSupport(
challengeString: sessionId,
gcp: 123456789, // Android only
);
```

### `generateAssertion` (iOS Only)

Generates an assertion for a previously attested device.

**Parameters:**
- `keyID` (required): Key identifier from initial attestation
- `clientData` (required): Unique data to sign (min 16 bytes when hashed)

**Returns:**
- Base64-encoded assertion string

**Example:**
```dart
final assertion = await deviceIntegrity.generateAssertion(
keyID: savedKeyID,
clientData: sessionData,
);
```

**Throws:**
- `PlatformException` with code `UNSUPPORTED_PLATFORM` on non-iOS platforms

## Error Handling

```dart
try {
final token = await deviceIntegrity.getAttestationServiceSupport(
challengeString: sessionId,
);
} on PlatformException catch (e) {
switch (e.code) {
case '-3':
print('Invalid arguments');
case '-4':
print('Device does not support attestation (simulator or old device)');
case '-5':
print('Attestation failed');
default:
print('Error: ${e.message}');
}
}
```

**Server:**<br>
I am very proud to provide you with an open-source service that you can [Attest your implementations in server side](https://github.com/Erluan/app_device_integrity_server).<br>
It checks from which platform the token is sent and verifies the token, providing very important information about risks, enabling better decision-making to reinforce the security of your services.<br>
Feel comfortable to fork it, or clone it, and customize it, according to your business demands.
## Additional Resources

For more information about App Attest and Play Integrity, you can access the docs from Apple and Google in the links bellow:<br>
**Apple App Attest:**<br>
https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity

**Google Play Integrity:**<br>
https://developer.android.com/google/play/integrity

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## Support

For issues and questions:
- GitHub Issues: [Create an issue](https://github.com/Erluan/app_device_integrity/issues)
- Discord: [Join our server](https://discord.gg/8GEp4dgM)

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
2 changes: 2 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
Expand Down
39 changes: 31 additions & 8 deletions example/integration_test/plugin_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,42 @@
// For more information about Flutter integration tests, please see
// https://docs.flutter.dev/cookbook/testing/integration/introduction

import 'package:app_device_integrity/app_device_integrity.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'package:app_device_integrity/app_device_integrity.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('getPlatformVersion test', (WidgetTester tester) async {
final AppDeviceIntegrity plugin = AppDeviceIntegrity();
final String? version = await plugin.getPlatformVersion();
// The version string depends on the host platform running the test, so
// just assert that some non-empty string is returned.
expect(version?.isNotEmpty, true);
testWidgets('getAttestationServiceSupport test', (WidgetTester tester) async {
const plugin = AppDeviceIntegrity();
final challengeString =
'test_challenge_${DateTime.now().millisecondsSinceEpoch}';

try {
final attestationToken = await plugin.getAttestationServiceSupport(
challengeString: challengeString,
);

// If we get here without exception, attestation is supported
// Verify the token is returned
expect(attestationToken, isNotNull);
expect(attestationToken, isA<String>());
debugPrint('Attestation successful: $attestationToken');
} on PlatformException catch (error) {
// On iOS simulator and some devices, App Attest is not available
// Error code -4 means "Failed to initialize AppDeviceIntegrity"
if (error.code == '-4') {
debugPrint(
'App Attest not supported on this device/simulator (expected)');
// This is expected on simulator, test passes
expect(error.message, contains('Failed to initialize'));
} else {
// Other errors should fail the test
rethrow;
}
}
});
}
Loading