packages

Secure Store

Encrypted storage for small secrets such as sign-in tokens and API keys. On iOS values go into the Keychain. On Android they are encrypted with an AES-256-GCM key that lives in the Android Keystore. You can require Face ID, Touch ID or a fingerprint before a value is read.

  • pyxpo-secure-store
  • v0.1.0a1
  • Native: Swift + Kotlin
  • Like expo-secure-store

Install

Install the package into the same environment as pyxpo.

$pip install pyxpo-secure-store

Add secure-store to modules in your app config, then import it in your code.

pyproject.toml
[tool.pyxpo]
modules = ["secure-store"]
python
import pyxpo_secure_store as SecureStore

This module has native code. After you add it, run pyxpo run or pyxpo build again so it is compiled into the app. Hot reload alone is not enough.

Platforms

iOSKeychain
AndroidKeystore key, ciphertext in SharedPreferences
Desktop previewIn memory only, not encrypted

Quick start

app/index.py
import pyxpo_secure_store as SecureStore

async def sign_in(token):
    await SecureStore.set_item_async("auth.token", token)

async def current_token():
    return await SecureStore.get_item_async("auth.token")   # str or None

async def sign_out():
    await SecureStore.delete_item_async("auth.token")

Configuration

These options set the text iOS shows when it asks the user for access. Each has a default, so you only set the ones you want to change.

OptionInfo.plist keyDefault
faceIDPermissionNSFaceIDUsageDescriptionUse Face ID to unlock your saved sign-in details.
pyproject.toml
[tool.pyxpo.module_options.secure-store]
faceIDPermission = "Use Face ID to unlock your saved sign-in details."

Permissions

pyxpo adds these to Info.plist and AndroidManifest.xml for you when the module is listed.

iOSNSFaceIDUsageDescription, used only when you set requireAuthentication.
AndroidUSE_BIOMETRIC.

API reference

Functions

All functions are coroutines. camelCase aliases exist: getItemAsync, setItemAsync, deleteItemAsync, isAvailableAsync.

await set_item_async(key, value, options=None)

Saves a string. Use json.dumps for objects.

await get_item_async(key, options=None)→ str | None

Reads a value. Returns None when the key is missing.

await delete_item_async(key, options=None)

Removes a value.

await is_available_async()→ bool

True on iOS and Android when the module is linked. False on the desktop.

await can_use_biometric_authentication_async()→ bool

True when the device can protect values with biometrics.

Types

Results and options are plain Python dicts with these keys.

Options

KeyTypeDescription
keychainServicestrGroups values. Use the same value to read what you wrote.
requireAuthenticationboolAsks for biometrics before reading. Android 10 or newer.
authenticationPromptstrText shown in the biometric prompt.
keychainAccessibleintiOS only. When the value can be read. Use one of the constants below.
accessGroupstriOS only. Keychain access group shared with other apps.

keychainAccessible constants

KeyTypeDescription
AFTER_FIRST_UNLOCK0After the first unlock since restart.
AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY1Same, and never moved to a new device.
ALWAYS2Always readable.
WHEN_PASSCODE_SET_THIS_DEVICE_ONLY3Only while a passcode is set.
ALWAYS_THIS_DEVICE_ONLY4Always, never moved to a new device.
WHEN_UNLOCKED5Only while the device is unlocked.
WHEN_UNLOCKED_THIS_DEVICE_ONLY6Same, and never moved to a new device.

Examples

Protect a value with biometrics

Ask for Face ID or a fingerprint before the value is returned.

python
import pyxpo_secure_store as SecureStore

PROTECTED = {
    "requireAuthentication": True,
    "authenticationPrompt": "Unlock your account",
}

async def save_pin(pin):
    if await SecureStore.can_use_biometric_authentication_async():
        await SecureStore.set_item_async("pin", pin, PROTECTED)

async def read_pin():
    return await SecureStore.get_item_async("pin", PROTECTED)

Store a small object

Values are strings, so encode objects as JSON.

python
import json
import pyxpo_secure_store as SecureStore

async def save_session(session):
    await SecureStore.set_item_async("session", json.dumps(session))

async def load_session():
    raw = await SecureStore.get_item_async("session")
    return json.loads(raw) if raw else None

Good to know

  • Keys may contain letters, digits, '.', '-' and '_'.
  • Keep values to a few KB. Use pyxpo-storage or pyxpo-file-system for larger data.
  • On iOS, Keychain items stay after the app is deleted. Clear them on first launch if that matters to you.
  • On the desktop preview values are kept in memory only, so you can test the flow without a device.