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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ migrate_working_dir/
**/doc/api/
.dart_tool/
build/
.build/
.swiftpm/
92 changes: 60 additions & 32 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,62 @@
# Changelog

All notable changes to this project will be documented in this file.

## [1.1.0] - 2025-06-11

### ✨ Added
- ✅ iOS support using `keychain` for persistent device identification
- Automatic fallback to UUID stored in the Keychain on iOS
- Improved platform abstraction layer
- Updated README and documentation

---

## [1.0.0] - 2025-06-10

### ✨ Added
- Initial release of `persistent_device_id` 🎉
- Support for Android devices using:
- `MediaDrm.deviceUniqueId` for hardware-based device IDs (API 18+)
- Fallback with securely stored UUID using Android Keystore + EncryptedSharedPreferences
- Public method: `PersistentDeviceId.getDeviceId()` to retrieve the unique ID
- Example app demonstrating usage

### ✅ Platform support
- ✅ Android (API 21+)
- 🚧 iOS: not yet implemented (planned)

### 🔐 Security
- Encrypted storage using AndroidX Security library
- No need for runtime permissions
- Persistent across uninstalls (thanks to MediaDrm)

---
## 2.0.0 - 2026-06-03

### Changed
- Raised the package SDK floor to Dart `^3.11.0` and Flutter `>=3.41.0`.
- Kept the public API stable as `PersistentDeviceId.getDeviceId()`.
- Routed the Dart API through the platform interface and removed scaffolded
`getPlatformVersion` code.
- Updated AndroidX Security Crypto to stable `1.1.0`.
- Lowered Android minSdk to 21.
- Changed Android fallback writes to synchronous, checked persistence and added
migration from app-private fallback storage into encrypted preferences.
- Moved iOS source into a Swift Package Manager-friendly layout while keeping
CocoaPods support.
- Updated iOS Keychain storage to use a service-scoped item and migrate the
legacy account-only item from earlier releases.
- Made iOS Keychain reads status-aware so temporary storage failures return
`null` instead of creating a second identifier.
- Rewrote README documentation with clearer guarantees, platform differences,
and persistence limitations.
- Refreshed package metadata, tests, and the example app for publish readiness.
- Kept the example integration-test plugin available to Flutter 3.44 release
builds so the generated Android plugin registrant compiles successfully.

### Added
- Swift Package Manager manifest for iOS.
- Privacy manifest bundling through both Swift Package Manager and CocoaPods.
- Dart tests for API stability, repeated calls, method-channel forwarding, and
nullable platform responses.
- Android native unit tests for `getDeviceId` and unknown method handling.
- Native persistence tests for failed writes, fallback migration, corrupted
values, legacy Keychain migration, and unavailable storage.

### Migration from 1.x
- The Dart API remains `PersistentDeviceId.getDeviceId()`.
- Dart `^3.11.0` and Flutter `>=3.41.0` are now required.
- The iOS deployment target increases from 12.0 to 13.0.
- Existing iOS account-only Keychain IDs are migrated automatically on the
first call after upgrading. A readable legacy ID remains valid even if the
migration write cannot complete.
- Android app-private fallback IDs are migrated into encrypted preferences when
encrypted storage becomes available.
- A `null` result means the native implementation could not obtain or durably
persist an ID. Method channel registration errors continue to throw.

## 1.1.0 - 2025-06-11

### Added
- iOS support using Keychain for persistent device identification.
- Automatic fallback to a generated UUID stored in the Keychain on iOS.
- Improved platform abstraction layer.
- Updated README and documentation.

## 1.0.0 - 2025-06-10

### Added
- Initial release of `persistent_device_id`.
- Android support using `MediaDrm.deviceUniqueId` where available.
- Fallback UUID storage using Android Keystore and encrypted shared preferences.
- Public method: `PersistentDeviceId.getDeviceId()`.
- Example app demonstrating usage.
Empty file added Configure
Empty file.
175 changes: 109 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,52 @@
<img src="https://raw.githubusercontent.com/maeltoukap/persistent_device_id/refs/heads/main/assets/persistent_device_id_logo.png" alt="Persistent Device ID Logo" width="200"/>

# 📱 persistent\_device\_id
# persistent_device_id

A Flutter plugin that provides a **unique, persistent, and secure** device identifier—**even after reinstalling the app or resetting the device**.
Supports **Android** and **iOS** using system-level cryptography and secure storage.
A Flutter plugin that returns a persistent, app-scoped device identifier for
Android and iOS.

---
The public API stays intentionally small:

## ✨ Features
```dart
final deviceId = await PersistentDeviceId.getDeviceId();
```

## What This ID Is

`persistent_device_id` returns an identifier that is stable across repeated app
launches on the same platform installation. It is designed for app diagnostics,
fraud signals, rate limiting, abuse prevention, and other cases where a stable
client-side installation/device signal is useful.

## What This ID Is Not

* 🔒 Generates a **unique and persistent ID per device**
* ♻️ Persists across app reinstalls, cache wipes, and even factory resets (when possible)
* 🛡️ Uses **MediaDrm**, **Android Keystore**, and **EncryptedSharedPreferences** on Android
* 🍏 Uses **Keychain** on iOS
* 🚫 Requires **no runtime permissions**
* 📦 Simple, asynchronous API
This value is not a proof of identity, authentication credential, advertising
identifier, or guaranteed hardware serial number. Do not use it as the only
security control for accounts, payments, licensing, or access decisions.

---
The identifier can change when platform storage is cleared, a device is reset,
operating system behavior changes, or an app is restored/migrated in a way that
does not preserve the underlying storage.

## 📦 Installation
## Supported Platforms

Add this to your `pubspec.yaml`:
| Platform | Support | Storage strategy |
| -------- | ------- | ---------------- |
| Android | Yes | MediaDrm when available, otherwise generated UUID in encrypted app storage |
| iOS | Yes | Generated UUID stored in Keychain |

macOS, Windows, Linux, and Web are intentionally not declared in this release.
Browser storage cannot provide the same security or persistence guarantees, and
desktop support should be added only with platform-specific persistence and
tests.

## Installation

Add the package to your `pubspec.yaml`:

```yaml
dependencies:
persistent_device_id: <latest_version>
persistent_device_id: ^2.0.0
```

Then run:
Expand All @@ -33,87 +55,108 @@ Then run:
flutter pub get
```

---

## 🛠️ Usage

### Import the package
## Usage

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

### Get the device ID

```dart
final deviceId = await PersistentDeviceId.getDeviceId();
print("Device ID: $deviceId");
Future<void> loadDeviceId() async {
final deviceId = await PersistentDeviceId.getDeviceId();
print('Device ID: $deviceId');
}
```

---
`getDeviceId()` returns `Future<String?>` to preserve the original public API.
Android and iOS return a generated ID only after it has been durably stored.
A `null` result means the native implementation could not obtain or persist a
stable ID. Method channel errors such as `MissingPluginException` are not
converted to `null` because they indicate an app integration or registration
problem.

## ⚙️ Supported Platforms
## Platform Details

| Platform | Support |
| -------- | ------- |
| Android | ✅ Yes |
| iOS | ✅ Yes |
### Android

---
Android first attempts to read a Widevine `MediaDrm` device identifier. When
that is unavailable or fails, the plugin generates a UUID and stores it in
`EncryptedSharedPreferences`, protected by AndroidX Security and Android
Keystore where available.

## 🧠 How It Works
If encrypted storage cannot be initialized or written, the plugin uses
app-private `SharedPreferences`. On a later call, an existing app-private ID is
migrated into encrypted storage when it becomes available, without changing the
returned ID. New IDs are returned only after a synchronous storage write
succeeds. If neither store can persist the ID, the plugin returns `null`.

This plugin uses **different secure layers per platform** to persist a device-unique identifier:
Minimum Android SDK: 21. AndroidX Security Crypto `1.1.0` supports API 21 and
later. The
[AndroidX Security release notes](https://developer.android.com/jetpack/androidx/releases/security)
note that AndroidKeyStore is not used by the library on API 21 and 22.

### Android
### iOS

1. Attempts to derive a hardware-based ID from [`MediaDrm`](https://developer.android.com/reference/android/media/MediaDrm) (API ≥ 18).
2. If `MediaDrm` is not supported or fails (e.g. on rooted/custom ROM devices), falls back to:
iOS generates a UUID and stores it in Keychain using a service-scoped generic
password item. Version 2.0.0 also migrates the legacy account-only Keychain item
used by earlier releases so existing apps can keep their previous identifier.
This migration is attempted automatically on the first `getDeviceId()` call
after upgrading. If the migration write fails, the readable legacy ID is still
returned.

* A generated UUID
* Stored in [`EncryptedSharedPreferences`](https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences)
* Protected by the [`Android Keystore`](https://developer.android.com/training/articles/keystore)
The Keychain item uses `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`, so it
is intended to stay on the same physical device and not migrate through backups
to another device. Missing or corrupted entries are regenerated, but the new ID
is returned only after the Keychain write succeeds. Temporary Keychain
unavailability returns `null` rather than creating a second identity.

### iOS
Minimum iOS version: 13.0.

## Migration From 1.x

* Uses the [`Keychain`](https://developer.apple.com/documentation/security/keychain_services) to securely store and persist a generated UUID.
Version 2.0.0 preserves `PersistentDeviceId.getDeviceId()`, but raises the
minimum tooling and platform requirements:

---
- Dart `^3.11.0` and Flutter `>=3.41.0` are required.
- The iOS deployment target increases from 12.0 to 13.0.
- Existing iOS account-only Keychain IDs are migrated to the service-scoped
entry on first access.
- Android app-private fallback IDs are migrated to encrypted preferences when
encrypted storage becomes available.
- Consumers should continue handling `null`, which now specifically means no
durable native ID could be obtained.

## ✅ Android Requirements
## Persistence Limits

* **minSdkVersion**: 21
* **compileSdkVersion**: 34
* No special permissions needed
The ID is persistent, but not immutable:

---
- App data clearing can reset Android fallback storage.
- iOS Keychain behavior after uninstall can vary by OS version, app group, and
installation history.
- Factory reset can reset identifiers.
- Device restore, backup migration, or OS policy changes can reset identifiers.
- Rooted, jailbroken, emulated, or heavily customized devices can behave
differently.

## 🚧 Limitations
If your app needs a durable user identity, use your own authenticated backend
identity and treat this package as an additional device/install signal.

* `MediaDrm` only available on Android **API ≥ 18**
* On some custom or rooted ROMs, `MediaDrm` may be unreliable
* Factory reset will remove the ID unless hardware-backed
* On iOS, Keychain-based ID may reset **if iCloud Keychain is disabled** or device is **restored without backup**
## Apple Packaging

---
The iOS implementation supports modern Flutter Apple packaging with Swift
Package Manager-friendly source layout while keeping CocoaPods support for
projects that have not migrated yet.

## 🔍 Example
## Example

Clone the repository and run the example app:
Run the bundled example app:

```bash
cd example
flutter run
```

---

## 📄 License

MIT License. © 2025 Mael Toukap.

---
The example shows loading, refreshing, copying, and displaying error/null states
for the device ID.

## 🙋‍♂️ Contributing
## License

Contributions are welcome! Please open an issue or submit a pull request on [GitHub](https://github.com/maeltoukap/persistent_device_id)
MIT License. See [LICENSE](LICENSE).
69 changes: 0 additions & 69 deletions android/build.gradle

This file was deleted.

Loading