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
- Explicit path - starts with
/or containsidentity.json - Vault ID - looks up
~/.sikkerkey/vaults/{vaultId}/identity.json SIKKERKEY_IDENTITYenv - path to identity file- 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
| Property | Type | Description |
|---|---|---|
machineId | String | Machine UUID |
machineName | String | Hostname from bootstrap |
vaultId | String | Vault this identity belongs to |
apiUrl | String | SikkerKey API URL |
Method Reference
| Method | Returns | Description |
|---|---|---|
SikkerKey(vaultOrPath?) | SikkerKey | Create client (companion invoke) |
SikkerKey.listVaults() | List<String> | List registered vault IDs (companion) |
getSecret(secretId) | String | Read a secret value |
getFields(secretId) | Map<String, String> | Read structured secret |
getField(secretId, field) | String | Read 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) | Unit | Watch a secret for changes |
unwatch(secretId) | Unit | Stop watching a secret |
setPollInterval(seconds) | Unit | Set poll interval (min 10s) |
close() | Unit | Stop 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
| Variable | Description |
|---|---|
SIKKERKEY_IDENTITY | Path to identity.json - overrides vault lookup |
SIKKERKEY_HOME | Base config directory (default: ~/.sikkerkey) |
Dependencies
| Dependency | Version | Purpose |
|---|---|---|
kotlinx-serialization-json | >=1.7.3 | JSON 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.