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. 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.currentKeyval keys: List<Key> = authn.keys
val current: Key? = authn.currentKeykeys 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
| State | Payload | What your UI does |
|---|---|---|
idle | — | Nothing yet. |
confirming | keys: [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. |
succeeded | revokedKeys: [RevokedKeyNotice] | Terminal success. |
cancelled | — | Terminal. |
failed | reason: FailureReason | Terminal. |
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() // abandonflow.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, and terminated: Bool. Surface these to the user: an entry here means an enrollment they thought they had is gone.
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
| State | Payload | What your UI does |
|---|---|---|
idle | — | Nothing yet. |
confirming | key: Key | Confirm dialog for this one key. |
authenticatingBiometric | — | Biometric prompt. |
submitting | — | Posting. |
succeeded | — | Terminal. The key is gone. |
cancelled | — | Terminal. |
failed | reason: FailureReason | Terminal. |
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.
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.
Related
- Registration — minting the first key.
- Errors —
AuthnRuntimeErrorand failure reasons. - Reference —
Key,RevokedKeyNotice,Branding,AuditEntry. - Signing key rotation — the server-side counterpart.
Registration flow
Enroll a user and mint a device key with the SenseCrypt Mobile SDK — email PIN verification, the self-signup branch with attribute collection and consent, and face capture.
UI components
The SenseCrypt Mobile SDK's bundled QR-scan and face-capture surfaces for SwiftUI, UIKit, and Jetpack Compose — plus the FaceCaptureDriver contract for building your own.