SenseCrypt Docs
SDKs & appsMobile SDK

Key management

Rotate and revoke device keys with the SenseCrypt Mobile SDK — the make-before-break rotation ceremony, the rotation hint, and single-key revocation.

A device holds one key per enrolled identity/application pair, with its private key in the device's secure hardware. Two flows manage them: rotation replaces the key material for every key on the device, and revocation removes a single key.

Both are short flows: they emit a confirmation state carrying what's affected, wait for approve(), gate on a biometric, and submit.

Listing what the device holds

let keys: [Key] = authn.keys
let current: Key? = authn.currentKey
val keys: List<Key> = authn.keys
val current: Key? = authn.currentKey

keys only ever contains usable keys — records whose underlying hardware key was wiped are filtered out, and keys that survived an app reinstall are cleared by open(). Use it to decide whether to offer a rotate/revoke UI at all.

Key rotation

Rotation is a make-before-break ceremony: new key material is generated and recorded under temporary aliases before the old keys are retired, so a device is never locked out mid-rotation.

Starting a rotation

let flow = try authn.startKeyRotationFlow()

Task {
    for await state in flow.stateStream() {
        handle(state)
    }
}
val flow = withContext(Dispatchers.IO) { authn.startKeyRotationFlow() }

lifecycleScope.launch {
    flow.state.collect { state -> handle(state) }
}

KeyRotationFlow.state is a StateFlow, so it does expose .value.

startKeyRotationFlow() throws AuthnRuntimeError.NoKeys when there is nothing to rotate. Hide the rotate action when keys is empty — but still handle the throw, because the last key can be revoked between your check and the tap.

Rotation states

StatePayloadWhat your UI does
idle—Nothing yet.
confirmingkeys: [Key]Pre-confirm dialog. Lists every key that will rotate.
authenticatingBiometric—The system biometric prompt is up.
generatingNewKeys—Generating key material. Show "this may take a moment" — it is slow on older devices.
submitting—Posting to the portal.
succeededrevokedKeys: [RevokedKeyNotice]Terminal success.
cancelled—Terminal.
failedreason: FailureReasonTerminal.

The confirming payload carries every key the device holds, so your dialog can say "Rotate 3 keys" and show which applications and email addresses are affected.

Approving

flow.approve()   // proceed
flow.cancel()    // abandon
flow.approve()
flow.cancel()

Reading the result

succeeded(revokedKeys:) lists every dead key the ceremony discovered and cleaned up — keys the server had already revoked or terminated, which were dropped rather than rotated. The list is empty when every key rotated cleanly.

Each RevokedKeyNotice carries email, clientId, appLabel, terminated: Bool and replaced: Bool. Surface these to the user: an entry here means an enrollment they thought they had is gone. replaced is true when the server said why — the user added the app on another device under a tenant that allows one device per user — so say the account moved rather than that an administrator removed it. The branding sync reports the same keys as BrandingSyncSummary.replacedKeys.

succeeded with a non-empty list can be emitted without any portal round-trip at all — if every local key turned out to be dead, there was nothing left to rotate. Don't assume success implies a successful network call.

The rotation hint

The portal signals, on any signed response, that this device is still using its previous key after a rotation overlap. The SDK captures that signal and exposes it as a one-shot flag:

if authn.takeKeyRotationHint() {
    // Converge onto the new key.
    let flow = try authn.startKeyRotationFlow()
}
if (authn.takeKeyRotationHint()) {
    val flow = withContext(Dispatchers.IO) { authn.startKeyRotationFlow() }
}

Poll it at a natural checkpoint — after a successful sign-in is the usual choice. Reading it clears it, but the portal re-sets it on the next request made with the old key, so a missed prompt self-corrects on the following sign-in. You do not need to persist it.

Revocation

Revocation removes one key, identified by its id.

let flow = try authn.startRevocationFlow(keyId: key.id)

Task {
    for await state in flow.stateStream() {
        handle(state)
    }
}
val flow = withContext(Dispatchers.IO) { authn.startRevocationFlow(key.id) }

lifecycleScope.launch {
    flow.state.collect { state -> handle(state) }
}

Throws AuthnRuntimeError.UnknownKey(keyId) if the id isn't held by this device.

Revocation states

StatePayloadWhat your UI does
idle—Nothing yet.
confirmingkey: KeyConfirm dialog for this one key.
authenticatingBiometric—Biometric prompt.
submitting—Posting.
succeeded—Terminal. The key is gone.
cancelled—Terminal.
failedreason: FailureReasonTerminal.

Drive it with approve() and cancel(), same as rotation.

Revocation is irreversible. The user must re-enroll to use this device for that identity again. Make the confirmation dialog say so — the confirming state hands you the Key precisely so you can name the affected application and email.

A key can also be revoked from the server side: an administrator can revoke a user's device from the admin console or the Management API. The device is not notified at that moment. Its next signed request for that identity, such as a sign-in or an approval, fails with deviceKeyRevoked (or deviceKeyTerminated when the user no longer has an application they could enroll through), and the SDK removes that key from the device. Treat it as a re-enrollment trigger, as described in errors: after deviceKeyRevoked the user can enroll this device again.

Branding for key lists

To render a key list with the right logo per entry:

let branding: Branding? = authn.brandingFor(keyId: key.id)
val branding: Branding? = authn.brandingFor(key.id)

Returns the cached branding for that key, or nil — in which case fall back to a default icon or a monogram. See Branding.

Audit log

let entries: [AuditEntry] = authn.auditLog(limit: 50)
val entries: List<AuditEntry> = authn.auditLog(limit = 50)

A local, device-side record of SDK activity — timestamp, kind, optional identityId, and a note. limit defaults to 50. Useful for an in-app activity screen.

On this page