---
title: "Keep tokens and keys in the Keychain or · Agent Wranglers"
url: https://agentwranglers.se/playbook/mobile-apps/data-on-the-device/keychain-keystore
updated: 2026-10-21
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Keep tokens and keys in the Keychain or Keystore

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Data on the device](https://agentwranglers.se/playbook/mobile-apps/data-on-the-device.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers). Updated 2026-10-21.

- ID: MOB-19
- Level: Required
- Chapter: [Data on the device](https://agentwranglers.se/playbook/mobile-apps/data-on-the-device)
- Rule: Store session tokens, refresh tokens and keys only in the iOS Keychain or behind a key in the Android Keystore. Never in plain preferences, files or JavaScript storage.
- Risk: An account or system taken over
- How: Plain app storage ends up in backups and on rooted phones. On iOS, save tokens in the Keychain with an accessibility class such as `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`. Such items stay off other devices restored from a backup. On Android, create a key in the Android Keystore and encrypt the token with it before you write it to app storage. Cross-platform apps use a plugin that wraps both, and check its settings pick these classes. Tokens are issued and ended on the server, as [let people sign out of one device or all of them](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/revoke-sessions) says.
- Check: Search the code for writes of tokens to `UserDefaults`, `SharedPreferences`, `AsyncStorage` or files. None remain. On a test device backup, no token appears in plain text.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### Plain text example

```swift
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrService as String: "com.example.fittrack.session",
    kSecAttrAccount as String: "refresh-token",
    kSecValueData as String: Data(refreshToken.utf8),
    kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError(status) }
```

### Kotlin example

```kotlin
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(
    KeyGenParameterSpec.Builder("session-key", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .build()
)
val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply { init(Cipher.ENCRYPT_MODE, keyGenerator.generateKey()) }
val sealed = cipher.iv + cipher.doFinal(refreshToken.toByteArray())
```

## Sources

- [OWASP MASVS-STORAGE-1](https://mas.owasp.org/MASVS/controls/MASVS-STORAGE-1/)
- [Apple, kSecAttrAccessibleWhenUnlockedThisDeviceOnly](https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlockedthisdeviceonly)

## Related rules

- [Let people sign out of one device or all of them](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/revoke-sessions) in Accounts
- [Keep secrets out of device backups](https://agentwranglers.se/playbook/mobile-apps/data-on-the-device/backup-exclusions) in Mobile apps
- [Encrypt local data that holds personal data](https://agentwranglers.se/playbook/mobile-apps/data-on-the-device/encrypted-local-data) in Mobile apps

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/mobile-apps/data-on-the-device/keychain-keystore">Agent Wranglers mobile apps playbook</a>`. In code you write, add a comment next to it with the same link.

- Next: [Ship no secrets in the app package](https://agentwranglers.se/playbook/mobile-apps/data-on-the-device/no-secrets-in-app)
- [Data on the device](https://agentwranglers.se/playbook/mobile-apps/data-on-the-device)
