Kotlin / JVM SDK

Read secrets from any Kotlin or Java application using Ed25519 machine authentication.

Installation

Gradle

dependencies {
    implementation("com.sikker:sikkerkey-sdk:1.0.0")
}

Maven

<dependency>
    <groupId>com.sikker</groupId>
    <artifactId>sikkerkey-sdk</artifactId>
    <version>1.0.0</version>
</dependency>

Single dependency: kotlinx-serialization-json. Requires Java 17+.

Quick Start

import com.sikker.key.sdk.SikkerKey

val sk = SikkerKey("vault_abc123")
val secret = sk.getSecret("sk_a1b2c3d4e5")

The SDK loads the machine identity from ~/.sikkerkey/vaults/{vaultId}/identity.json, signs every request with the machine's Ed25519 private key, and returns the decrypted value. All methods are synchronous (blocking).

Client Creation

// Explicit vault ID
val sk = SikkerKey("vault_abc123")

// Direct path to identity file
val sk = SikkerKey("/etc/sikkerkey/vaults/vault_abc123/identity.json")

// Auto-detect from SIKKERKEY_IDENTITY env or single vault on disk
val sk = SikkerKey()

Throws ConfigurationException if the identity is missing, the key can't be loaded, or multiple vaults exist without a specified vault ID.

Reading Secrets

Single Value

val apiKey = sk.getSecret("sk_stripe_prod")

Structured (Multiple Fields)

val fields = sk.getFields("sk_db_prod")
val host = fields["host"]       // "db.example.com"
val password = fields["password"] // "hunter2"

Throws SecretStructureException if the secret value is not a JSON object.

Single Field

val password = sk.getField("sk_db_prod", "password")

Throws FieldNotFoundException if the field doesn't exist. The error message includes available field names.

Listing Secrets

// All secrets this machine can access
val secrets = sk.listSecrets()
for (s in secrets) {
    println("${s.id}: ${s.name}")
}

// Secrets in a specific project
val projectSecrets = sk.listSecretsByProject("proj_production")

Returns List<SecretListItem> with id, name, fieldNames (nullable), and projectId (nullable).

Export

// All secrets as a flat map
val env = sk.export()
// {API_KEY=sk-live-..., DB_CREDS_HOST=db.example.com, DB_CREDS_PASSWORD=s3cret}

// Scoped to a project
val env = sk.export("proj_production")

// Inject into system properties
sk.export().forEach { (k, v) -> System.setProperty(k, v) }

Structured secrets are flattened: SECRET_NAME_FIELD_NAME.

Watching for Changes

Watch secrets for real-time updates. When a secret is rotated, updated, or deleted, the callback fires with the new value.

sk.watch("sk_db_credentials") { event ->
    when (event.status) {
        WatchStatus.CHANGED -> {
            // event.value has the new value
            // event.fields has parsed key-value pairs for structured secrets
            Database.configureCredentials(
                event.fields!!["username"]!!,
                event.fields["password"]!!
            )
        }
        WatchStatus.DELETED -> println("Secret deleted")
        WatchStatus.ACCESS_DENIED -> println("Access revoked")
        WatchStatus.ERROR -> println("Error: ${event.error}")
    }
}

Polling starts automatically on the first watch() call and runs on a background daemon thread. Default interval is 15 seconds (server enforces a 10-second minimum).

sk.setPollInterval(30) // seconds
sk.unwatch("sk_db_credentials") // stop watching one secret
sk.close() // stop all watches

SikkerKey implements AutoCloseable.

Java Interop

import com.sikker.key.sdk.SikkerKey;

var sk = SikkerKey.Companion.invoke("vault_abc123");
var secret = sk.getSecret("sk_stripe_key");

var fields = sk.getFields("sk_db_prod");
var host = fields.get("host");

Multi-Vault

val prod = SikkerKey("vault_a1b2c3")
val staging = SikkerKey("vault_x9y8z7")

val prodKey = prod.getSecret("sk_api_key")
val stagingKey = staging.getSecret("sk_api_key")

List Registered Vaults

val vaults = SikkerKey.listVaults()
// ["vault_a1b2c3", "vault_x9y8z7"]

Companion object function.

Identity Resolution

  1. Explicit path - starts with / or contains identity.json
  2. Vault ID - looks up ~/.sikkerkey/vaults/{vaultId}/identity.json
  3. SIKKERKEY_IDENTITY env - path to identity file
  4. Auto-detect - single vault on disk

The vault_ prefix is added automatically if not present. Override base directory with SIKKERKEY_HOME.

Error Handling

import com.sikker.key.sdk.*

try {
    val secret = sk.getSecret("sk_nonexistent")
} catch (e: NotFoundException) {
    // 404 - secret doesn't exist
} catch (e: AccessDeniedException) {
    // 403 - machine not approved or no grant
} catch (e: AuthenticationException) {
    // 401 - invalid signature or unknown machine
} catch (e: ApiException) {
    // any other HTTP error
    println(e.httpStatus)
}

Exception Hierarchy

SikkerKeyException (extends RuntimeException)
├── ConfigurationException      - identity/key issues
├── SecretStructureException    - secret is not a JSON object (getFields)
├── FieldNotFoundException      - field not in structured secret (getField)
└── ApiException                - HTTP error (has httpStatus property)
    ├── AuthenticationException - 401
    ├── AccessDeniedException   - 403
    ├── NotFoundException       - 404
    ├── ConflictException       - 409
    ├── RateLimitedException    - 429
    └── ServerSealedException   - 503

All exceptions extend RuntimeException (unchecked).

Properties

PropertyTypeDescription
machineIdStringMachine UUID
machineNameStringHostname from bootstrap
vaultIdStringVault this identity belongs to
apiUrlStringSikkerKey API URL

Method Reference

MethodReturnsDescription
SikkerKey(vaultOrPath?)SikkerKeyCreate client (companion invoke)
SikkerKey.listVaults()List<String>List registered vault IDs (companion)
getSecret(secretId)StringRead a secret value
getFields(secretId)Map<String, String>Read structured secret
getField(secretId, field)StringRead single field
listSecrets()List<SecretListItem>List all accessible secrets
listSecretsByProject(projectId)List<SecretListItem>List secrets in a project
export(projectId?)Map<String, String>Export as env map
watch(secretId, callback)UnitWatch a secret for changes
unwatch(secretId)UnitStop watching a secret
setPollInterval(seconds)UnitSet poll interval (min 10s)
close()UnitStop all watches, shut down polling

Retry Behavior

429 and 503 responses are retried up to 3 times with exponential backoff (1s, 2s, 4s). Each retry uses a fresh timestamp and nonce. Network errors (IOException) are also retried.

Environment Variables

VariableDescription
SIKKERKEY_IDENTITYPath to identity.json - overrides vault lookup
SIKKERKEY_HOMEBase config directory (default: ~/.sikkerkey)

Dependencies

DependencyVersionPurpose
kotlinx-serialization-json>=1.7.3JSON parsing

Ed25519 signing uses java.security.Signature (JDK built-in, Java 17+). HTTP uses java.net.HttpURLConnection. No external HTTP client.

All HTTP requests have 15-second connect and read timeouts. HTTPS enforced for non-localhost.