Quick Start

Get Kiskis running in your iOS app in under 60 seconds.

1. Create an Account

Sign up at the Kiskis Dashboard and note your provisioning key.

2. Upload Your Config

curl -X POST https://api.kiskis.dev/v1/admin/config/upload \
  -H "Authorization: Bearer sk_prod_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"version":"*","config":{"api_keys":{"stripe":"sk_live_..."}}}'

3. Add the Swift Package

In Xcode: File → Add Package Dependencies → enter:

https://github.com/kiskis/kiskis-ios-sdk

4. Fetch Config

import Kiskis

let kiskis = KiskisClient(
    teamId: "YOUR_TEAM_ID",
    bundleId: "com.your.app"
)
let config = try await kiskis.fetchConfig()
let stripeKey = config.string("api_keys.stripe")
That's it. The SDK handles App Attest, JWT tokens, Keychain caching, and background refresh automatically.

Core Concepts

How Kiskis Works

Kiskis solves the "bootstrap problem" — how does an app get API keys without embedding them in the binary?

  1. Developer uploads config (API keys, endpoints, feature flags) to the Kiskis vault via dashboard, CLI, or CI/CD.
  2. App launches and the SDK performs hardware attestation via Apple App Attest (Secure Enclave).
  3. Server verifies the attestation, confirming it's a genuine, unmodified app on a real device.
  4. Server returns the config, matched to the app's version number.
  5. SDK caches the config in the iOS Keychain, encrypted and hardware-bound.

Key Principles

  • Nothing is embedded. Your app ships with only your Team ID and Bundle ID (public, non-secret data).
  • Hardware proves identity. Apple's Secure Enclave generates a key pair that can never be extracted.
  • Offline-first. Cached config loads instantly. Network fetches happen in the background.
  • Version targeting. Different app versions can get different configs.

Security Layers

LayerMechanismPurpose
TransportTLS 1.3Encrypt in transit
App IdentityApp Attest (Secure Enclave)Prove genuine device + app
Replay PreventionsignCount + nonceEach request is unique
AuthorizationJWT (15 min TTL)Time-limited access
Path ObfuscationEd25519-signed S3 pathsCan't guess storage locations
Rate LimitingAPI Gateway + WAFPrevent abuse
Encryption at RestS3 SSE-KMSProtect stored configs

SDK Installation

Swift Package Manager (Recommended)

In Xcode, go to File → Add Package Dependencies and enter:

https://github.com/kiskis/kiskis-ios-sdk

Select the Kiskis library (core). Optionally add:

  • KiskisAnalytics — telemetry reporting (~1MB)
  • KiskisBinaryBlobs — large file downloads (~0.5MB)

Requirements

  • iOS 14.0+ (App Attest requires Secure Enclave)
  • Xcode 15+
  • Swift 5.9+

App Attest Capability

In Xcode, go to your target → Signing & Capabilities → + Capability → App Attest.

Simulator note: App Attest uses sandbox mode in the simulator. The SDK auto-detects this and uses sandbox attestation endpoints. Full hardware attestation requires a physical device.

Usage Guide

Basic Usage

import Kiskis

let kiskis = KiskisClient(
    teamId: "A1B2C3D4E5",
    bundleId: "com.myapp.weather"
)

// Fetch all config
let config = try await kiskis.fetchConfig()
let stripeKey = config.string("api_keys.stripe")
let maxUpload = config.int("limits.max_upload_mb")
let darkMode = config.bool("features.dark_mode")

Non-Blocking Initialization

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
        didFinishLaunchingWithOptions opts: ...) -> Bool {
        Task {
            do {
                let config = try await KiskisClient.shared?.fetchConfig()
                NotificationCenter.default.post(
                    name: .kiskisReady, object: config)
            } catch {
                handleOfflineState(error)
            }
        }
        return true // Don't block main thread
    }
}

Fallback Config (First Install Offline)

let kiskis = KiskisClient(
    teamId: "A1B2C3D4E5",
    bundleId: "com.myapp.weather",
    fallbackConfig: Bundle.main.url(
        forResource: "fallback_config",
        withExtension: "json")
)
Security warning: Fallback configs are embedded in the binary and are NOT protected by attestation. Never put API keys in the fallback — only non-sensitive defaults (endpoints, feature flags).

Per-User Data

Store user-specific data tied to their iCloud identity. No sign-in required.

// The SDK auto-detects the iCloud recordID
// Data is stored at a hashed path — anonymous, cross-device

Offline & Caching

How Caching Works

  1. First run: SDK attests with Apple, fetches config, encrypts and stores in Keychain.
  2. Subsequent runs: Returns cached config instantly. Refreshes in background.
  3. Offline: Returns cached config. Sets isStale flag if past TTL.
  4. Long offline (>7 days default): Cache expires. SDK returns error or fallback.

Cache Policy Configuration

let kiskis = KiskisClient(
    teamId: "A1B2C3D4E5",
    bundleId: "com.myapp.weather",
    cachePolicy: .init(
        maxStaleness: .days(7),     // Trust cache for 7 days offline
        backgroundRefresh: true,     // Silent refresh when online
        onStaleConfig: .warnAndUse   // .warnAndUse | .failHard | .useSilently
    )
)

Staleness Policies

PolicyBehaviorBest For
.warnAndUseReturn stale config with isStale = trueMost apps (default)
.failHardThrow error if staleFinancial apps
.useSilentlyReturn stale config, hide stalenessGames, media apps

Apple Outage Grace Mode

If Apple's attestation servers go down, the SDK extends the JWT TTL by 1 hour and serves cached config. Retries in background. Your app keeps working.

Zero-Knowledge Mode

In Zero-Knowledge mode, your config is encrypted on your machine before it ever touches our servers. We store and deliver opaque ciphertext. We literally cannot read your secrets.

How It Works

  1. CLI encrypts config locally using AES-256-GCM (key derived via HKDF)
  2. Encrypted blob is uploaded to Kiskis
  3. Server stores opaque ciphertext — cannot read or validate it
  4. SDK fetches ciphertext, decrypts locally with the vault key

CLI Usage

# Encrypt and upload in one step
kiskis upload --file secrets.json --key $KEY --ver "*" \
  --encrypt --vault-pass "MyVaultKey"

# Or encrypt separately
kiskis encrypt --file secrets.json --vault-pass "MyVaultKey"
kiskis upload --file secrets.json.enc --key $KEY --ver "*"

SDK Usage

let kiskis = KiskisClient(
    teamId: "A1B2C3D4E5",
    bundleId: "com.myapp.weather",
    zeroKnowledge: .enabled(key: "MyVaultKey")
)
// fetchConfig() automatically decrypts locally
let config = try await kiskis.fetchConfig()
Tradeoff: In ZK mode, the server can't preview, validate, or filter your config. Selective fetching (scope parameter) is not available — the entire blob must be decrypted client-side.

Dashboard

The Kiskis dashboard at dashboard.kiskis.dev provides:

  • Setup: Enter your Team ID and Bundle ID, paste your provisioning key
  • Config Editor: Upload and manage configs with version targeting
  • Provisioning Keys: Create and revoke keys for CLI/CI access

Authentication is via Cognito (email + password, optional MFA).

CLI Tool

npx kiskis-cli --help

Commands

CommandDescription
kiskis uploadUpload config for a version pattern
kiskis encryptEncrypt a file locally (ZK mode)
kiskis decryptDecrypt a file locally (testing)
kiskis keys:createCreate a provisioning key
kiskis keys:revokeRevoke a provisioning key
kiskis config:viewView the config manifest
kiskis config:deleteDelete a version pattern or all configs

Example

# Create a key
kiskis keys:create --team-id A1B2C3 --bundle-id com.my.app

# Upload default config
kiskis upload --file config.json --key sk_prod_... --ver "*"

# Upload v2-specific config
kiskis upload --file v2-config.json --key sk_prod_... --ver "2.*"

# View what's deployed
kiskis config:view --key sk_prod_...

CI/CD Integration

GitHub Actions

- name: Deploy Config to Kiskis
  uses: kiskis/deploy-action@v1
  with:
    api-key: ${{ secrets.KISKIS_PROVISIONING_KEY }}
    config-file: ./config/production.json
    version: '*'

Zero-Knowledge in CI/CD

- name: Deploy Encrypted Config
  uses: kiskis/deploy-action@v1
  with:
    api-key: ${{ secrets.KISKIS_PROVISIONING_KEY }}
    config-file: ./config/secrets.json
    version: '*'
    zero-knowledge: 'true'
    vault-pass: ${{ secrets.KISKIS_VAULT_PASS }}

Other CI Systems

Use the CLI directly:

npx kiskis-cli upload --file config.json --key $KISKIS_KEY --ver "*"

Version Targeting

Different app versions can receive different configs. This lets you roll out new API endpoints to v3.0+ while v2.x stays on the old ones.

Version Patterns

PatternMatchesExample Use
*All versionsDefault config
2.*Any 2.x.xMajor version override
2.1.*Any 2.1.xMinor version override
2.1.3Exactly 2.1.3Hotfix for specific build

Matching Priority

When the app sends version 2.1.3, the server tries in order:

  1. Exact: 2.1.3
  2. Patch wildcard: 2.1.*
  3. Minor wildcard: 2.*
  4. Default: *

First match wins.

Security Architecture

Threat Model

ThreatMitigation
Decompile app for keysNo keys in binary; attestation prevents unauthorized use
Fake app clonesApp Attest verifies binary integrity + Secure Enclave
Bot/script calls APINo Secure Enclave = rejected
Replay attackssignCount must increment; nonces are one-time
Probe S3 for dataEd25519-signed hash paths; bucket denies ListBucket
MITM interceptTLS 1.3; optional certificate pinning
Kiskis employee accessS3 Deny-all-except-Lambda; KMS encryption; CloudTrail audit

S3 Path Obfuscation

Config is stored at cryptographically signed paths. Even if the bucket were exposed, an attacker sees only random hex strings with no way to map them to developers.

Input:  "A1B2C3.com.myapp" (TeamID.BundleID)
Sign:   Ed25519.sign(masterKey, input)
Path:   SHA256(signature) → "a7f3b9c2e1d4..."

App Store Review Guide

What to Disclose

  • In your App Review notes, mention that the app downloads configuration at runtime via Kiskis.
  • The SDK includes a PrivacyInfo.xcprivacy manifest. No user data is collected.
  • The SDK uses only public Apple APIs (DeviceCheck framework).

Privacy Nutrition Labels

CategoryCollected?
Contact InfoNo
Health & FitnessNo
Financial InfoNo
LocationNo
IdentifiersNo (App Attest is anonymous)
Usage DataNo
DiagnosticsOptional (KiskisAnalytics module)
Important: Do not use Kiskis to enable features that weren't reviewed by Apple (e.g., hidden in-app purchases). This will get your app rejected.

Testing Matrix

EnvironmentSecure EnclaveAttestationConfig Source
Xcode SimulatorNo (mocked)SandboxReal server (sandbox)
Physical device (debug)YesSandboxReal server (sandbox)
TestFlightYesProductionReal server (production)
App StoreYesProductionReal server (production)

Sandbox vs Production

The SDK auto-detects the environment:

#if DEBUG
KeySAS.environment = .sandbox
#else
KeySAS.environment = .production
#endif

The server accepts both sandbox and production attestation tokens, validating against the appropriate Apple certificate chain.

Delivery API

Base URL: https://vyy382jkp3.execute-api.us-west-1.amazonaws.com/v1

POST /auth/challenge

Get a one-time nonce for the attestation ceremony.

Response: { "nonce": "base64string" }

POST /auth/attest

Exchange App Attest attestation for JWT tokens.

Body: {
  "attestationObject": "base64",
  "keyId": "string",
  "nonce": "string",
  "teamId": "A1B2C3D4E5",
  "bundleId": "com.my.app"
}
Response: {
  "accessToken": "jwt",
  "refreshToken": "jwt",
  "expiresIn": 900
}

POST /auth/refresh

Refresh an expired access token.

Body: { "refreshToken": "jwt" }
Response: { "accessToken": "jwt", "refreshToken": "jwt", "expiresIn": 900 }

GET /config?version=2.1.3

Fetch config matched to the app version. Requires Authorization: Bearer {accessToken}.

Optional: &scope=api_keys.stripe,features for selective fetching.

Response: {
  "config": { ... },
  "matchedPattern": "2.*",
  "requestedVersion": "2.1.3"
}

GET /blob/{key}

Get a presigned S3 URL for a binary blob. Expires in 5 minutes.

GET/PUT /user/data?user_id=xxx

Get or save per-user data identified by iCloud recordID.

Management API

All management endpoints require Authorization: Bearer {provisioning_key}.

POST /admin/config/upload

Body: { "version": "2.*", "config": { ... } }
Response: { "message": "...", "totalPatterns": 3, "allPatterns": [...] }

GET /admin/config

View the full config manifest (all version patterns).

DELETE /admin/config?version=2.*

Delete a specific version pattern. Omit version to delete all.

POST /admin/keys/create

Body: { "teamId": "...", "bundleId": "...", "keyName": "..." }
Response: { "key": "sk_prod_...", "message": "Save this key..." }

POST /admin/keys/revoke

Body: { "key": "sk_prod_..." }

POST /admin/kill-switch

Body: { "versions": ["2.1.3"], "enabled": false, "reason": "..." }

POST /admin/emergency-revoke

Body: { "version": "*", "force_refresh_ttl": 300 }

POST /admin/blob/upload

Body: { "blob_key": "model.bin", "data_base64": "...", "content_type": "..." }