Integrate KeyLockr QR login and end-to-end encrypted storage with one stable app_tag.
All external SSO requests send the effective app_tag. Without a custom tag, the effective value is the numeric Service ID, but it is still sent in app_tag; there is no external svc_id field.
Device bindings use safe_id + app_tag + sign_pk; AppData uses safe_id + app_tag.
The name shown by the Safe always comes from the title registered in MyDeveloper. QR callers cannot override it.
The user authorizes with the KeyLockr Safe. Your backend verifies the device through an IP-allowlisted endpoint.
One Safe and tag share one ciphertext while every computer keeps an independent keypair and device row.
| Field | Rule |
|---|---|
title | Authoritative app name shown by the Safe. |
server_ips | Caller IP allowlist for app_verify and sso_user_pubkey. |
app_tag | Optional custom identity. Blank uses the numeric Service ID. A custom value must be globally unique, non-numeric, and not system-reserved. A service without a custom tag can set one later in MyDeveloper; once a custom tag is set it is locked and cannot be changed. |
data_approved | Allows AppData for new bindings; otherwise auth-only. |
App Attest is not part of this flow, and there is no app_attest_challenge API. An app_tag is a public identifier, not a client secret.
POST https://api.keylockr.app/v3/handshake
Content-Type: application/octet-stream
// MessagePack
{
sign_pk: Uint8Array, // Ed25519, 32 bytes
enc_pk: Uint8Array, // X25519, 32 bytes
app_tag: "wuse"
}
// -> { _res: "ok", tmp_id: string }
SSO-only requests may omit name. AppData or Access Point requests must send the physical client/device name. The Safe always resolves the authoritative app title from MyDeveloper through app_tag.
sig = base64url(nacl.sign("tmp.{tmp_id}.{ts}", sign_sk))
wss://api.keylockr.app/v3/ws?sig={sig}
// Choose one current QR form:
sso://login?tmp_id={tmp_id}
keylockr://sso?tmp_id={tmp_id}
Show only one QR code per authorization. Both forms use the same tmp_id; SSO, AppData, and Access Point capabilities come from server-side handshake state and never require a second QR.
Authorization completion is WebSocket-only, not polled. A brief reconnect replays the completed result while tmp_id remains valid.
{
action: "app_auth_result",
status: "done", // must check; anything else is not a completed authorization
app_id: string,
safe_id: string,
capabilities: string[], // e.g. ["sso"]; verify it matches what you requested
user_nickname: string,
user_enc_pk: Uint8Array,
ap_access?: { full_access: boolean, scopes: string[] }, // Access Point only
account_key_for_ap?: Uint8Array, // Access Point only
data_plain?: Uint8Array, // AppData only
data_encrypted?: Uint8Array, // AppData only
ver?: string // AppData only
}
POST https://api.keylockr.app/v3/app_verify
{
app_tag: "wuse",
app_id: string,
safe_id: string,
sign_pk: string // base64, same public key as handshake
}
// -> { _res: "ok", valid: boolean, nickname?: string }
The call must originate from a registered server_ips address. Check _res first; a hard error is not the same as valid:false.
AppData uses KPS identity app.{app_id}. Keep the recovered filekey in memory; KeyLockr stores only an encrypted filekey and ciphertext.
action: "app_req_filekey", body: {}
// -> { status: "done" | "safe_auth_required" | "denied",
// data_filekey?: Uint8Array, ver?: string }
// After Safe approval:
{ action: "app_filekey_result", status: "done",
data_filekey: Uint8Array, data_enc: Uint8Array, ver: string }
action: "app_get_data", body: {}
// -> {
// file_id: string,
// ver: string,
// data_plain: Uint8Array,
// data_encrypted: Uint8Array
// }
action: "app_set_data", body: {
ver: string, // exact version from the last read
data_enc: Uint8Array
}
// -> { file_id: string, ver: string } // new version
// stale version:
// -> { _res: "err", code: "file_ver_conflict" }
ver is mandatory. On conflict, read the latest data, merge in your own business logic, and write with the new version. KeyLockr does not retry, merge, create revisions, or open a transaction for this operation.
| Device identity | (safe_id, app_tag, sign_pk) |
|---|---|
| Shared data identity | (safe_id, app_tag) |
sign_sk/enc_sk; never copy private keys between devices.data_filekey, ciphertext, or version.| Endpoint/action | Identity | Purpose |
|---|---|---|
POST /v3/handshake | none | Start authorization with app_tag. |
GET /v3/ws?sig= | signed query | KPS WebSocket. |
POST /v3/app_verify | service IP allowlist | Verify device and user. |
app_req_filekey | app.* | Request the shared filekey from the Safe. |
app_get_data | app.* | Read ciphertext and version. |
app_set_data | app.* | Version-CAS write. |
POST /v3/sso_user_pubkey | service IP allowlist | Approved plugin services fetch a bound Safe public key with app_tag. |
GET /svc_pk/{app_tag} | public | Service-to-service encryption public key. |
The Safe internally uses app_scan_info and app_add to complete a binding; third-party clients do not call them directly.
For integration questions, contact infosafex.cloud