Device Key
Encrypt with a key generated on the device and kept in Vault, so a value stored locally cannot be opened anywhere else.
On this page
Basic Usage#
import 'package:magic_encryption/magic_encryption.dart';
Future<void> main() async {
final String secret = await Crypt.encryptWithDeviceKey('my-user-token');
final String value = await Crypt.decryptWithDeviceKey(secret);
print(value); // my-user-token
}
Register core's VaultServiceProvider in providers. Device-key mode reads and writes the key through the Vault facade and needs no app.key, so it works without EncryptionServiceProvider and without APP_KEY.
Quick Reference#
| Member | Returns | Meaning |
|---|---|---|
Crypt.encryptWithDeviceKey(value) |
Future<String> |
Encrypts under the device key, creating it on first use. |
Crypt.decryptWithDeviceKey(payload) |
Future<String> |
Decrypts under the device key. Throws MagicDecryptException like Crypt.decrypt. |
Crypt.hasDeviceKey() |
Future<bool> |
Whether Vault holds a device key. Never creates one. |
Crypt.generateDeviceKey() |
Future<void> |
Replaces the key. Everything encrypted under the old one is lost. |
Crypt.clearDeviceKey() |
Future<void> |
Deletes the key from Vault. Everything encrypted under it is lost. |
The payload is the same base64(iv):base64(ciphertext) as Crypt, and the same limits apply: no MAC, and encryptWithDeviceKey('') throws RangeError.
How the Key Is Made#
On first use the key is read from Vault under the name magic:device_encryption_key. When it is absent, 32 characters are drawn from A-Z, a-z and 0-9 with a secure random source and stored there. The encrypter built from it is then cached in memory for the life of the process.
How safe the key is depends on Vault's store on that platform: the Keychain on iOS and macOS, the Keystore on Android, and a weaker store on web.
Lifecycle#
- The key lives as long as the secure storage entry does. On Android, clearing app data or uninstalling the app removes it. On iOS the Keychain item can survive a reinstall, because neither
Vaultnor this package wipes it on first run. Once the key is gone and the app has restarted (a running process keeps the key in memory and does not readVaultagain), the ciphertext it protected cannot be opened. generateDeviceKey()andclearDeviceKey()both update the in-memory cache as well asVault, so the next call uses the new state at once.- The key never leaves the device, so a server or another device cannot decrypt a payload encrypted here; on the same iOS device a reinstall can, when the Keychain item survived and you kept the ciphertext. Send data to a server through the transport, not through this cipher; see Wire Format.
Gotchas#
| Mistake | Fix |
|---|---|
Forgetting VaultServiceProvider |
Register core's provider; the key is stored through Vault. |
Calling generateDeviceKey() to "refresh" the key |
It orphans every existing value. Call it only when the data is disposable. |
Decrypting a Crypt.encrypt payload with the device key, or the other way round |
The two modes use different keys; keep each value with the mode that wrote it. |
| Treating the device key as backed up | It is lost with the secure storage. Keep nothing here that you cannot recreate. |
| Reaching for device-key mode to share data with a server | The server cannot read it. Send the data over the transport instead; a key shared with a server would ship inside the app (see Crypt). |
Changing the macOS keychain an app uses (signing with a provisioning profile after ad-hoc builds, or macOsUsesDataProtectionKeychain on MagicVaultService) |
The two macOS keychains share no items and a miss looks like "never stored", so the next call generates a new device key and everything encrypted under the old one is unreadable. Pin the keychain on an app that already holds a key. |
| Leaking the cached key between tests | Call Crypt.clearDeviceKey() in tearDown; see Testing. |