Skip to main content

Custom Storage Client

Overview

The StorageClient is the persistence boundary between the SDK and your app. The SDK never writes keyshare bytes to disk itself — it calls your implementation — which makes your StorageClient the single source of truth for keyshares. Back it with whatever your platform offers: SQLite, ObjectBox, the file system, secure hardware, etc.

Every keyshare is addressed by an opaque keyId string, so your implementation is a key/value store keyed by keyId. The SDK stores one ReconcileStoreDao per key, holding two slots plus the id:

FieldMeaning
keyIdThe stable identifier for the key across all operations.
currentKeyshareThe active, reconciled share used for signing, derivation, and export. null until the key is first reconciled.
stagedKeyshareA freshly produced share awaiting reconciliation. null in the steady state.

The two-slot design is what makes keyshare transitions crash-safe — see Keygen with Reconcile and KeyRefresh with Reconcile. Your only job is to persist and return the record faithfully.

The contract

  • write(dao) receives a complete snapshot of the record. Persist every field exactly as given, including nulls — when the SDK commits a reconciliation (or a delete(keyId)) it writes a record with a null slot, and your store must clear the old bytes rather than keep them.
  • read(key) returns the record stored under key, or null when nothing is stored. key is always the keyId.
  • keyType: KeyType — one of ECDSA, EdDSA, or MLDSA — declares the algorithm family this client serves. If your app runs more than one algorithm, provide a separate client per family (each with a matching keyType) rather than multiplexing one store.

Production requirements

Keyshares are long-lived, high-value secrets. The in-memory map shown below is fine for tests and development, but a production implementation must be:

  • Confidential — encrypt keyshare bytes at rest (e.g. with a key from the Android Keystore); never log them.
  • Durable — survive process death and device restarts.
  • Atomic — a partially applied write must never surface on the next read; use an atomic replace or a transaction.
  • Concurrency-safe — tolerate overlapping calls without corrupting state.

Extend SilentShardStorageClient — a convenience base for StorageClient that narrows read to return ReconcileStoreDao directly, so you skip casting the generic StorageDao.

Implementing a Custom StorageClient

The demo below is an in-memory map keyed by keyId — useful for testing or development, but not suitable for production.

CustomStorageClient.kt
import com.silencelaboratories.silentshard.storage.KeyType
import com.silencelaboratories.silentshard.storage.StorageClient
import com.silencelaboratories.silentshard.storage.StorageDao
import com.silencelaboratories.silentshard.storage.silentshard.ReconcileStoreDao


class CustomStorageClient(
// The algorithm family this client stores keyshares for. Provide a separate
// client (with a matching keyType) per algorithm if you run more than one.
override val keyType: KeyType = KeyType.ECDSA,
) : StorageClient {

/**
* Representing an in-memory database keyed by keyId. In the real world this
* should be a SQL-based DB, secure storage, or custom hardware — it is up to
* the implementing app's use-case.
*/
private val entries = mutableMapOf<String, ReconcileStoreDao>()

/**
* Write the complete ReconcileStoreDao snapshot to storage. The dao holds the
* current state of the keyshare from the SDK's point of view; persist every
* field exactly as given, including nulls (the SDK uses them to clear state).
*/
override suspend fun write(dao: StorageDao) {
require(dao is ReconcileStoreDao) { "Expected ReconcileStoreDao" }
// Write to storage.
entries[dao.keyId] = dao
}

/**
* Read the record stored under [key] (always a keyId), or null if none exists.
*/
override suspend fun read(key: String): StorageDao? {
// Read from storage.
return entries[key]
}
}