Keychain

A keychain keeps the small secrets an app cannot put in its preferences: a session token, a password, an API key. day-part-keychain stores one under a key of your choosing in the platform’s own secure store and reads it back, with a protection level that says when the item may be read, including “only after the device verifies the person, every time”.

Add to your app

Defined in day-part-keychain. Add this to your app’s Cargo.toml [dependencies] table, using the same Day Git source as the rest of your app:

[dependencies]
day-part-keychain = { git = "https://github.com/daybrite/day.git" }

The implementation is selected by the compilation target; no toolkit feature flags or dependency entry in Day.toml are needed. An app that stores items at Protection::UserPresence also declares the biometrics permission (docs/permissions), which carries the sentence Face ID shows the first time it is used:

[permissions]
biometrics = true   # the reason is the catalog's `permission_biometrics` message
Platforms / toolkitsAvailability
iOS, macOSThe data protection keychain. An unsigned macOS development build falls back to the login keychain and says so (backend() returns login-keychain, support() Emulated); user presence is unavailable there.
AndroidFiles in the app’s no-backup directory, encrypted with keys the Android Keystore holds; user presence through the system’s biometric prompt (API 28 and later).
WindowsFiles under %LOCALAPPDATA%, protected with DPAPI for the signed-in account; user presence gated by Windows Hello (Emulated: the check is in front of the item, not bound to it).
Linux (GTK / Qt)The Secret Service (GNOME Keyring, KDE’s ksecretd, KeePassXC) through libsecret, loaded at run time. Unsupported on a session with none; no user-presence level.
HarmonyOSFiles in the app sandbox, encrypted with Universal Keystore keys; user presence through a key bound to user authentication.
WebNo store a page may rely on for a secret: Unsupported.

backend() identifies the selected storage implementation. It returns secret-service on Linux even when the session has no reachable service. Check support() for current availability.

Authoring

use day_part_keychain::{KeychainError, Protection, Secret};

// Store. Never shows a dialog, at any level.
day_part_keychain::set("session", &Secret::from(token), Protection::WhenUnlocked)?;

// Read, synchronously. A `UserPresence` item answers `NeedsPresence` here.
let token = day_part_keychain::get("session")?;
println!("{}", token.as_str().unwrap_or("<binary>"));

// Read an item that needs the person: the OS shows its dialog with the reason.
day::task(async move {
    match day_part_keychain::get_future("vault-key", &res::str::unlock_vault_reason().format()).await {
        Ok(secret) => open_vault(secret),
        Err(KeychainError::Cancelled) => {}
        Err(e) => status.set(e.to_string()),
    }
});

set(key, secret, protection) replaces any item under key, whatever its level. get(key) is synchronous and never shows a dialog. get_async(key, reason, callback) and get_future(key, reason) show the device’s verification dialog when the item needs it and resolve with the secret; an item at another level resolves without a dialog. remove(key), contains(key) and entries() (every key with its level, without the secrets) complete the API. A key is at most MAX_KEY_LEN bytes with no control characters; a secret at most MAX_SECRET_LEN (64 KiB).

Secret owns its bytes and zeroes them when dropped, compares in constant time, and prints as Secret(<n> bytes) so a stray {:?} never logs it. Build one from a string or bytes; read it with as_str() or as_bytes().

Protection levels

ProtectionReadableUse it for
WhenUnlocked (default)while the device is unlockeda session token the UI uses
AfterFirstUnlockfrom the first unlock after boot until reboot, locked screen includeda secret background work needs (a sync, a notification handler)
UserPresenceonly after the device verifies the person, each reada key that opens the app’s own vault

Apple and HarmonyOS distinguish the first two (WhenUnlocked items are unreadable while the device is locked); Android, Windows and Linux keep both readable whenever the app runs, since their stores unlock with the first sign-in. protection_support(level) says how a level is realized here: Native where the OS binds the item to it, Emulated where the crate gates the read without a binding (Windows user presence), Unsupported where nothing can honor it (Linux user presence, every level on the web). A UserPresence set needs a device passcode, PIN or password and fails with KeychainError::PasscodeNotSet without one. Writing never prompts; only reading does.

What an item is scoped to

Items belong to the app that wrote them, on the device that holds them. Each platform scopes them by the app’s identity: the bundle on Apple, the package on Android, the signed-in account and the app id on Windows, the app id as a Secret Service attribute on Linux, the sandbox on HarmonyOS. Nothing is backed up or synchronized, because the keys that encrypt the items never leave the device. Apple keeps keychain items across a reinstall of the app; the other platforms remove them with it.

Errors

KeychainError names the outcomes an app acts on: NotFound, NeedsPresence (a protected item read with the synchronous get), Cancelled and AuthFailed (the dialog), Lockout, PasscodeNotSet, Unreadable (the item exists but its key was invalidated or its file damaged; remove clears it), Locked (a keyring whose unlock dialog was dismissed, an Apple item read while the device is locked), Unavailable (no Secret Service, no logon session), Unsupported, and Platform(message) for the rest.

Threads

The synchronous calls finish in milliseconds on every platform but Linux, where a locked keyring makes the Secret Service show its unlock dialog during the call; UI code there prefers get_async, which works on a thread of its own. Completions run where the platform delivers them; get_future awaited under day::task resumes on the UI thread (docs/async).

Per-platform native realization

Platform guidance: Apple: keychain services; Android: the Android Keystore system; Windows: Data Protection API; freedesktop: the Secret Service API; HarmonyOS: Universal Keystore Kit.

OSAPIdependency
iOS, macOSSecItemAdd / SecItemCopyMatching / SecItemDelete on generic-password items with kSecUseDataProtectionKeychain; SecAccessControlCreateWithFlags with userPresence; an LAContext through kSecUseAuthenticationContextobjc2-security, objc2-core-foundation, objc2
AndroidAES-256-GCM under an AndroidKeyStore key from KeyGenParameterSpec; a user-authentication key wraps UserPresence items and is used through BiometricPrompt.CryptoObject; files under getNoBackupFilesDir()a Java arm (day-bridge)
WindowsCryptProtectData / CryptUnprotectData over one file per item; day-part-biometrics (Windows Hello) in front of UserPresence itemswindows
Linuxsecret_password_storev_sync / lookupv_sync / clearv_sync / searchv_sync under a schema of the crate’s own, libsecret-1.so.0 loaded with dlopennone at build time
HarmonyOSOH_Huks_GenerateKeyItem / OH_Huks_InitSession / OH_Huks_FinishSession (AES-256-GCM, user-authentication-bound keys) over one file per item; day-part-biometrics turns the keystore’s challenge into the token the key acceptslibhuks_ndk.z.so (the public NDK)
Web—none (Unsupported)

How each platform realizes the levels

  • iOS and macOS: WhenUnlocked is kSecAttrAccessibleWhenUnlockedThisDeviceOnly, AfterFirstUnlock is kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, and UserPresence is an access control created with kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly and the userPresence flag: a biometric or the passcode, each read, not invalidated by a change of enrollment. The synchronous get hands the keychain an LAContext with interactionNotAllowed set, so the keychain refuses instead of prompting; get_async hands it one carrying the reason, and the system shows its dialog during SecItemCopyMatching. The data protection keychain needs the keychain-access-groups entitlement a signed app has; an unsigned cargo run or development launch on macOS falls back to the login keychain, which has no access control.
  • Android: one file per item under the app’s no-backup directory, so a backup restored to another device does not carry ciphertext nothing can open. Items at the first two levels are AES-256-GCM under one keystore key. UserPresence items are AES-256-GCM under a per-item key that an RSA keystore key pair wraps: encryption with the public key needs no prompt, and decryption with the private key is allowed only through a cipher the system prompt authorized, with the device credential accepted alongside a biometric. Keys are created by set, never by a read, so a wiped keystore reads as Unreadable rather than silently starting over.
  • Windows: DPAPI binds the bytes to the signed-in account on this machine and adds its own integrity check; another account, or another machine, cannot open them, and an administrator’s password reset makes them Unreadable. UserPresence is Windows Hello in front of the read, reported as Emulated because DPAPI itself does not know about the check. A set at that level confirms Hello is set up first, so no item is stored that nothing could read.
  • Linux: one Secret Service item per key in the default collection, under the schema dev.daybrite.day.keychain with application and key attributes, the secret stored as base64 text so every implementation behind the interface keeps it intact. A locked keyring is unlocked by the service’s own dialog during the call; a dismissed dialog reads as Locked. A session with no Secret Service (a headless runner, a container) reports Unsupported. There is no user-presence level: no desktop API lets an app verify the person.
  • HarmonyOS: three AES-256-GCM keys the Universal Keystore generates on first use, one per level: WhenUnlocked at the ECE storage level (usable only while unlocked), AfterFirstUnlock at CE, and UserPresence at ECE with user authentication required for decryption only, so a set never prompts. The item’s key name is the cipher’s associated data. Reading a protected item opens a keystore session (which issues a challenge), shows the user-authentication dialog through day-part-biometrics with that challenge, and finishes the session with the token the dialog returned. Removing the lock-screen password invalidates the protected key.

What it shows about the extension system

Like day-part-battery and day-part-haptics, this is a headless crate with no UI Piece. It exercises three of the extension mechanisms at once (docs/extending): a daybridge Java arm for Android (docs/bridge), the manifest-permission overlay (USE_BIOMETRIC and ACCESS_BIOMETRIC contributed by the crate, the Face ID reason declared by the app), and a part depending on a part (day-part-biometrics on Windows and HarmonyOS, the way day-part-downloads rides on day-part-http). The Day Showcase’s Security page stores, reads and removes an item at each level.