Documentation Index
Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
Use this file to discover all available pages before exploring further.
Registration is a one-time-per-(app_id, device) handshake that establishes a hardware-backed device identity with the auth service. After it completes, the SDK can sign every outgoing request without further server round-trips.
Steps (normative)
1. SDK → POST /auth/v1/device/challenge { app_id }
2. SDK ← { challenge, expires_at, ttl_seconds }
3. SDK gen keypair in hardware (alias: synheart_auth_{app_id})
4. SDK compute binding_nonce = SHA256(challenge + base64(public_key))
5. SDK attest with binding_nonce (App Attest / Play Integrity)
6. SDK → POST /auth/v1/device/register
{ app_id, public_key, challenge, platform, proof,
device_local_id? }
7. Server verify challenge (GETDEL), recompute nonce, verify proof,
store (app_id, device_id, public_key, platform)
8. SDK ← { device_id, status }
When used through Synheart Core, steps 1, 6, and 8 are performed by the Synheart runtime, which calls the SDK’s crypto callbacks for steps 3, 4, 5.
1. Request a challenge
POST /auth/v1/device/challenge
Content-Type: application/json
{ "app_id": "com.example.app" }
Response:
{
"challenge": "<base64, ≥32 bytes>",
"expires_at": "<ISO8601>",
"ttl_seconds": 90
}
Challenge TTL is 90 seconds (RFC §7.3). Single-use, server-enforced. If the SDK fails to complete steps 2–7 within the TTL, it must request a new challenge.
2. Generate the keypair
| Platform | Algorithm | Storage | Alias |
|---|
| iOS | ECDSA P-256 | Secure Enclave (SecKey with kSecAttrTokenIDSecureEnclave) | synheart_auth_{app_id} |
| Android | ECDSA P-256 | Android Keystore (KeyGenParameterSpec with hardware backing) | synheart_auth_{app_id} |
The key MUST be non-exportable. The Dart SDK never sees the private key; only signRequest results.
3. Compute binding nonce
binding_nonce = SHA256( challenge_bytes || base64(public_key_bytes) )
This binds the attestation proof to the specific public key the SDK is registering. The server recomputes the nonce in step 7 and rejects mismatches — preventing attestation reuse with a different key.
| Platform | API | Input |
|---|
| iOS | DCAppAttestService.attestKey(...) | keyId from key generation, clientDataHash = binding_nonce |
| Android | IntegrityManager.requestIntegrityToken(...) | nonce = binding_nonce |
Returns the attestation proof (Apple) or integrity token (Google). The SDK does not validate it — that’s the server’s job in step 7.
The Dart facade returns AttestationUnavailable if the platform doesn’t support attestation.
5. Register with auth service
POST /auth/v1/device/register
Content-Type: application/json
{
"app_id": "com.example.app",
"public_key": "<base64 P-256 public key, X.509 SPKI form>",
"challenge": "<the same challenge from step 1>",
"platform": "ios" | "android",
"proof": "<attestation token>",
"device_local_id": "<optional UUID for local correlation, non-authoritative>"
}
Server verifies (RFC §7.2.6):
GETDEL the challenge from Redis (single-use, atomic).
- Recompute
SHA256(challenge + base64(public_key)) and compare with the attestation proof’s nonce field.
- Verify proof against Apple/Google attestation APIs.
- Persist
(app_id, device_id, public_key, platform, status, registered_at).
- Return
{ device_id, status }.
status is one of "registered" | "pending" | "rejected".
6. Persist locally
The SDK stores per app_id:
| Field | iOS | Android |
|---|
device_id | Keychain | EncryptedSharedPreferences |
| Key alias / handle | Secure Enclave | Keystore |
platform | Local storage | Local storage |
registered_at | Local storage | Local storage |
key_rotated_at | Local storage | Local storage |
clock_offset_ms | Local storage | Local storage |
Never persisted: private key bytes (live only in hardware), raw attestation tokens.
Idempotency
registerDevice(appId) is idempotent: subsequent calls return { status: alreadyRegistered, deviceId: <existing> } without touching the network. Call it at app start regardless — the SDK fast-paths when isRegistered(appId) is true.
Failure paths
| Failure | SDK behavior | Recovery |
|---|
| Challenge request 5xx / network | NETWORK_ERROR with backoff | Retry. |
| Challenge expired | CHALLENGE_EXPIRED | Re-fetch challenge. |
| Attestation unsupported | ATTESTATION_UNAVAILABLE | Cannot proceed without dev-mode bypass. |
| Attestation API rejects | ATTESTATION_FAILED | Retry once, then surface. |
Register 4xx with INVALID_CHALLENGE | server says nonce mismatch | Re-fetch challenge and restart. |
Register 4xx with INVALID_ATTESTATION | server rejects proof | Surface as ATTESTATION_FAILED. |
| Register 5xx / network | NETWORK_ERROR | Backoff + retry; always with a fresh challenge. |
| Already registered | ALREADY_REGISTERED | No-op. |
| Registration in progress | REGISTRATION_IN_PROGRESS | Caller already racing — wait. |
Rotation flow (RFC §8.3)
rotateKey(appId) replaces the signing key while preserving the device_id:
1. Generate new keypair: synheart_auth_{app_id}_next
2. Sign rotation request with the CURRENT key:
POST /auth/v1/device/rotate-key
Body: { app_id, device_id, new_public_key }
3. Server verifies signature against current public key
4. Server atomically replaces stored public key
5. Server returns { status: "rotated", effective_at: <unix> }
6. SDK promotes:
- delete old key from hardware
- rename _next → primary alias
- update key_rotated_at
7. On any failure, keep the old key active and retry later
Rotation policy (RFC §8.3): SDK SHOULD rotate every 90 days; MUST rotate immediately on platform key-compromise signals; server MAY reject keys older than its configured maximum age.
Dev / QA bypass (RFC §14)
Attestation bypass exists for emulator/CI and is strictly controlled:
- Server-side allowlist on
app_id + build channel (dev/staging only).
"development_integrity_allowed": true in platform-service.
- Mobile SDK MUST set
X-Synheart-Dev-Mode: true header so the server tracks dev vs. prod traffic.
- Bypass MUST be compile-time gated (
#if DEBUG / BuildConfig.DEBUG) — never runtime-toggleable.
There are no hidden bypasses. A production build that emits X-Synheart-Dev-Mode: true is treated as a security incident.