Key Management

Cryptographic key management powered by the Signal Protocol (X3DH). Register, rotate, backup, and manage encryption keys for end-to-end encrypted communication between users.

Overview

The Keys API adds 10 endpoints under /v1/keys/* for complete cryptographic key lifecycle management. Built on the Signal Protocol (X3DH) — the same protocol used by Signal, WhatsApp, and Wire — it enables true end-to-end encryption where the server never sees private keys.

Same base URL: All Keys endpoints use the same https://api.anchora.co.in/v1 base URL and the same Authorization: Bearer YOUR_API_KEY authentication as existing endpoints.

Key Types

Key Type Algorithm Purpose Lifetime
Identity Key Ed25519 Long-term identity — your "fingerprint" Permanent (until revoked)
Signed Pre-Key X25519 Enables offline key exchange Weekly/monthly rotation
One-Time Pre-Keys X25519 Forward secrecy per session Single use, then deleted
Ephemeral Key X25519 Per key-exchange, never stored Immediate discard

Use Cases

  • Secure Messaging: End-to-end encrypted chat (like Signal/WhatsApp)
  • Healthcare: HIPAA-compliant patient-doctor communication
  • Banking: Secure document exchange between bank and client
  • Legal: Encrypted contract sharing with tamper-proof audit trail
  • IoT: Secure device-to-gateway encrypted telemetry

Algorithms

Purpose Algorithm Key Size
Key exchange X25519 (Curve25519 DH) 32 bytes (256 bits)
Digital signatures Ed25519 32 bytes public, 64 bytes private
Symmetric encryption AES-256-GCM 32 bytes (256 bits)
Key derivation HKDF-SHA-256 Variable
Backup encryption Argon2id + AES-256-GCM Password-derived
Zero-Knowledge Architecture: Private keys are generated client-side and never sent to the server. The server only stores public keys and encrypted backups (opaque blobs it cannot decrypt).

Register Keys

POST /v1/keys/register

Register a user's public keys. This is the "I exist and I'm ready to receive encrypted messages" announcement. Must be called before any key exchange can happen.

Request Body

POST /v1/keys/register
curl -X POST https://api.anchora.co.in/v1/keys/register \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_abc123",
    "identityKey": "a1b2c3d4...64_hex_chars_ed25519_pubkey",
    "signedPreKey": {
      "keyId": "spk_a1b2c3d4e5f6g7h8",
      "publicKey": "f1e2d3c4...64_hex_chars_x25519_pubkey",
      "signature": "aa11bb22...128_hex_chars_ed25519_signature"
    },
    "oneTimePreKeys": [
      { "keyId": "opk_001", "publicKey": "11aa22bb...64_hex_chars" },
      { "keyId": "opk_002", "publicKey": "44dd55ee...64_hex_chars" }
    ],
    "deviceId": "device_xyz",
    "deviceName": "iPhone 15"
  }'

Parameters

FieldTypeRequiredDescription
userIdStringYesYour app-defined user ID
identityKeyStringYesEd25519 public key (64-char hex)
signedPreKeyObjectYesX25519 signed pre-key with keyId, publicKey, and signature
oneTimePreKeysArrayYes1-100 X25519 one-time pre-keys
deviceIdStringNoDevice identifier
deviceNameStringNoHuman-readable device name

Response (201)

Success response
{
  "success": true,
  "message": "Keys registered successfully",
  "data": {
    "userId": "user_abc123",
    "identityKeyFingerprint": "a1b2c3d4e5f6a1b2",
    "signedPreKeyId": "spk_a1b2c3d4e5f6g7h8",
    "oneTimePreKeysCount": 2,
    "status": "active",
    "registeredAt": "2026-03-08T10:00:00.000Z"
  }
}
409 Conflict: Returned if an active key set already exists for this userId. Revoke the existing keys first, then re-register.

Get Key Bundle

GET /v1/keys/bundle/:userId

Fetch a user's public key bundle for key exchange. Atomically consumes one one-time pre-key to ensure forward secrecy.

GET /v1/keys/bundle/:userId
curl https://api.anchora.co.in/v1/keys/bundle/user_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

Key bundle response
{
  "success": true,
  "data": {
    "userId": "user_abc123",
    "identityKey": "a1b2c3d4...64_hex",
    "signedPreKey": {
      "keyId": "spk_a1b2c3d4e5f6g7h8",
      "publicKey": "f1e2d3c4...64_hex",
      "signature": "aa11bb22...128_hex"
    },
    "oneTimePreKey": {
      "keyId": "opk_001",
      "publicKey": "11aa22bb...64_hex"
    },
    "remainingOneTimePreKeys": 1
  }
}
If no one-time pre-keys are available, "oneTimePreKey" will be null and a warning will indicate reduced forward secrecy. Replenish OPKs using the rotate endpoint.

Rotate Keys

POST /v1/keys/rotate

Rotate the signed pre-key and/or replenish one-time pre-keys. Old signed pre-key is saved in rotation history for existing sessions.

POST /v1/keys/rotate
curl -X POST https://api.anchora.co.in/v1/keys/rotate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_abc123",
    "newSignedPreKey": {
      "keyId": "spk_new_001",
      "publicKey": "bb11cc22...64_hex",
      "signature": "ee44ff55...128_hex"
    },
    "newOneTimePreKeys": [
      { "keyId": "opk_050", "publicKey": "aa00bb11...64_hex" }
    ]
  }'

Response (200)

Rotation response
{
  "success": true,
  "message": "Keys rotated successfully",
  "data": {
    "signedPreKeyRotated": true,
    "newSignedPreKeyId": "spk_new_001",
    "oneTimePreKeysAdded": 1,
    "totalOneTimePreKeysAvailable": 16,
    "rotatedAt": "2026-03-08T12:00:00.000Z"
  }
}
Best Practice: Rotate signed pre-keys weekly or monthly. Replenish one-time pre-keys when count drops below 5 to maintain forward secrecy.

Backup Keys

POST /v1/keys/backup

Store an encrypted private key backup. The client encrypts the private keys locally using Argon2id + AES-256-GCM before sending. The server stores only an opaque blob it cannot decrypt.

POST /v1/keys/backup
curl -X POST https://api.anchora.co.in/v1/keys/backup \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_abc123",
    "encryptedBundle": "YWJjZGVm...base64_encrypted_blob",
    "encryptionMetadata": {
      "algorithm": "argon2id+aes-256-gcm",
      "argon2Params": {
        "timeCost": 3,
        "memoryCost": 65536,
        "parallelism": 4
      },
      "nonce": "a1b2c3d4e5f6a1b2c3d4e5f6",
      "salt": "f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4"
    },
    "checksum": "e3b0c442...sha256_hex"
  }'

Response (200)

Backup response
{
  "success": true,
  "message": "Backup stored successfully",
  "data": {
    "version": 1,
    "storedAt": "2026-03-08T10:30:00.000Z"
  }
}

Recover Keys

POST /v1/keys/recover

Retrieve the encrypted backup blob. The client decrypts it locally with the user's password. The server never knows the password and cannot read the keys.

POST /v1/keys/recover
curl -X POST https://api.anchora.co.in/v1/keys/recover \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "user_abc123" }'

Response (200)

Recovery response
{
  "success": true,
  "data": {
    "encryptedBundle": "YWJjZGVm...base64_blob",
    "encryptionMetadata": {
      "algorithm": "argon2id+aes-256-gcm",
      "argon2Params": { "timeCost": 3, "memoryCost": 65536, "parallelism": 4 },
      "nonce": "a1b2c3d4e5f6a1b2c3d4e5f6",
      "salt": "f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4"
    },
    "checksum": "e3b0c442...",
    "version": 1,
    "backedUpAt": "2026-03-08T10:30:00.000Z"
  }
}

Revoke Keys

POST /v1/keys/revoke

Mark a user's keys as revoked. Use when a device is lost, stolen, or compromised. Existing sessions are unaffected, but no new key exchanges can use these keys.

POST /v1/keys/revoke
curl -X POST https://api.anchora.co.in/v1/keys/revoke \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_abc123",
    "reason": "Device compromised"
  }'

Response (200)

Revocation response
{
  "success": true,
  "message": "Keys revoked successfully",
  "data": {
    "userId": "user_abc123",
    "status": "revoked",
    "revokedAt": "2026-03-08T14:00:00.000Z",
    "reason": "Device compromised"
  }
}
Irreversible: Key revocation cannot be undone. After revoking, the user must register a new key set to resume encrypted communication.

Verify Key Status

GET /v1/keys/verify/:userId

Check if a user's key is valid and active. Returns status, fingerprint, and key health info. Use before sending sensitive data to confirm the recipient's keys are trustworthy.

GET /v1/keys/verify/:userId
curl https://api.anchora.co.in/v1/keys/verify/user_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (200)

Verification response
{
  "success": true,
  "data": {
    "userId": "user_abc123",
    "isValid": true,
    "status": "active",
    "identityKeyFingerprint": "a1b2c3d4e5f6a1b2",
    "registeredAt": "2026-03-08T10:00:00.000Z",
    "lastRotatedAt": "2026-03-08T12:00:00.000Z",
    "oneTimePreKeysRemaining": 15,
    "deviceName": "iPhone 15"
  }
}

List Keys

GET /v1/keys/list

List all registered keys for the project with filtering and pagination. Scoped to your project — you can only see keys within your own project.

GET /v1/keys/list
curl "https://api.anchora.co.in/v1/keys/list?status=active&page=1&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"

Query Parameters

ParameterTypeDefaultDescription
statusStringallFilter by status: active, revoked, expired, suspended
pageNumber1Page number
limitNumber20Results per page (max 100)
sortByStringcreatedAtSort field
sortOrderStringdescasc or desc

Response (200)

List response with pagination
{
  "success": true,
  "data": [
    {
      "userId": "user_abc123",
      "identityKeyFingerprint": "a1b2c3d4e5f6a1b2",
      "status": "active",
      "oneTimePreKeysRemaining": 15,
      "registeredAt": "2026-03-08T10:00:00.000Z",
      "lastRotatedAt": "2026-03-08T12:00:00.000Z",
      "deviceName": "iPhone 15"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 45,
    "totalPages": 3,
    "hasNextPage": true
  }
}

Establish Shared Secret

POST /v1/keys/shared-secret

Server-assisted X3DH key agreement. The server facilitates the public key exchange and returns the responder's bundle. The actual 32-byte shared secret is computed entirely client-side — the server never knows it.

POST /v1/keys/shared-secret
curl -X POST https://api.anchora.co.in/v1/keys/shared-secret \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "initiatorUserId": "user_alice",
    "responderUserId": "user_bob",
    "initiatorEphemeralKey": "cc11dd22...64_hex_x25519_ephemeral"
  }'

Response (200)

Shared secret response
{
  "success": true,
  "data": {
    "sessionId": "sess_a1b2c3d4...",
    "responderBundle": {
      "identityKey": "a1b2c3d4...64_hex",
      "signedPreKey": {
        "keyId": "spk_001",
        "publicKey": "f1e2d3c4...64_hex",
        "signature": "aa11bb22...128_hex"
      },
      "oneTimePreKey": {
        "keyId": "opk_003",
        "publicKey": "77001188...64_hex"
      }
    },
    "expiresAt": "2026-03-09T10:00:00.000Z"
  }
}
True E2E Encryption: The shared secret is computed client-side using 4 Diffie-Hellman operations (X3DH). Even Anchora's servers cannot derive or access the shared encryption key.

Audit Log

GET /v1/keys/audit

Complete audit trail of every key operation. Essential for security investigations, compliance requirements (HIPAA, SOC 2, GDPR), and debugging encryption issues.

GET /v1/keys/audit
curl "https://api.anchora.co.in/v1/keys/audit?userId=user_abc123&action=KEYS_ROTATED&limit=50" \
  -H "Authorization: Bearer YOUR_API_KEY"

Query Parameters

ParameterTypeDescription
userIdStringFilter by user ID
actionStringFilter by action type
startDateStringISO date for range start
endDateStringISO date for range end
pageNumberPage number (default: 1)
limitNumberResults per page (default: 50)

Audit Actions

ActionDescription
KEYS_REGISTEREDNew key set registered
KEYS_ROTATEDSigned pre-key rotated
KEYS_REVOKEDKeys marked as revoked
BUNDLE_FETCHEDSomeone fetched this user's bundle
BACKUP_CREATEDFirst backup stored
BACKUP_UPDATEDBackup replaced (new version)
BACKUP_RECOVEREDBackup retrieved
SHARED_SECRET_INITIATEDKey agreement started
SHARED_SECRET_COMPLETEDKey agreement finished
KEY_VERIFIEDKey validity checked
PREKEY_CONSUMEDOne-time pre-key used
PREKEYS_REPLENISHEDNew one-time pre-keys added

Response (200)

Audit log response
{
  "success": true,
  "data": [
    {
      "action": "KEYS_ROTATED",
      "resource": "USER_KEY",
      "userId": "user_abc123",
      "details": {
        "newSignedPreKeyId": "spk_002",
        "oneTimePreKeysAdded": 10
      },
      "ipAddress": "203.0.113.42",
      "status": "SUCCESS",
      "createdAt": "2026-03-08T12:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 12,
    "totalPages": 1,
    "hasNextPage": false
  }
}

Rate Limits

Endpoint GroupLimitNotes
All /v1/keys/* endpoints200 req/minPer API key
/v1/keys/bundle/:userId100 req/minStricter limit to prevent key enumeration

Full SDK Example

Here's a complete example showing two users establishing an encrypted channel using the Anchora SDK:

End-to-end encryption between two users
const { AnchoraClient } = require('@anchora/sdk');

// === Setup Client ===
const client = new AnchoraClient({
  apiKey: 'dcp_live_xxx',
  projectId: 'proj_123',
  baseURL: 'https://api.anchora.co.in'
});

// === User A: Generate & Register Keys ===
const keysA = client.keys.generateKeySet(20);
saveToSecureStorage('user_a_keys', keysA.privateKeys);

await client.keys.registerKeys(
  'user_a',
  keysA.registrationPayload,
  { deviceName: 'Alice iPhone' }
);

// === User A: Establish Shared Secret with User B ===
const session = await client.keys.establishSharedSecret(
  'user_a',
  'user_b',
  keysA.privateKeys.identityKey
);
// session.sharedSecret → 32-byte key (never sent to server)

// === Encrypt & Anchor ===
const message = { text: "Hello Bob!" };
const encrypted = await EncryptionService.encrypt(
  message, session.sharedSecret
);
const anchor = await client.anchor(message, { hashOnly: true });

// === Send to User B (via your transport) ===
sendToUserB({
  encrypted,
  anchorHash: anchor.hash,
  sessionId: session.sessionId
});

Error Responses

StatusError CodeDescription
400VALIDATION_ERRORInvalid key format, missing required fields
401UNAUTHORIZEDMissing or invalid API key
404NOT_FOUNDUser keys not found
409CONFLICTActive keys already exist for this user
429RATE_LIMITEDToo many requests
Error response format
{
  "success": false,
  "error": "Active keys already exist for this user",
  "errorCode": "CONFLICT"
}