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:
| Field | Meaning |
|---|---|
keyId | The stable identifier for the key across all operations. |
currentKeyshare | The active, reconciled share used for signing, derivation, and export. null until the key is first reconciled. |
stagedKeyshare | A 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 adelete(keyId)) it writes a record with anullslot, and your store must clear the old bytes rather than keep them.read(key)returns the record stored underkey, ornullwhen nothing is stored.keyis always thekeyId.keyType: KeyType— one ofECDSA,EdDSA, orMLDSA— declares the algorithm family this client serves. If your app runs more than one algorithm, provide a separate client per family (each with a matchingkeyType) 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
writemust never surface on the nextread; 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.
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]
}
}