feat(wallet): rewrite SSSS on account data + AES-256-GCM envelope
The Rust SDK removed the low-level SecretStoreWrapper.putSecret/getSecret API between 26.03.x and 26.04.x — it was an escape hatch we were using to pin arbitrary bytes into a Matrix 4S slot. The SDK maintainers never contracted that primitive; locking it down lets their recovery code evolve without worrying about third-party storage. This commit replaces that dependency with a self-contained design we own end-to-end, so future SDK moves no longer break our backup flow. ### Design - Slot: `com.sulkta.wallet.seed.v1` in Matrix account data. Our namespace, not a Matrix-spec 4S slot — we are NOT impersonating Matrix secret storage, we are holding our own opaque blob. - Envelope (JSON): version tag, algorithm tag, random 12-byte IV, GCM output (ciphertext || tag), AAD = slot name. AES-256-GCM via stock javax.crypto. AAD binds a blob to its slot so a blob can't be lifted from one namespace and successfully opened in another. - Key: derived from the user's existing Matrix recovery key via HKDF-SHA256 with info label "sulkta.wallet.seed.v1". The info label guarantees we never produce the same key bytes Matrix uses for its own crypto — same secret, different domain. - I/O: client.setAccountData(key, json) + client.accountData(key) via the SDK; the homeserver only ever sees the opaque encrypted blob. ### Files - api/walletsecretstorage/WalletSecretStorage.kt — new interface - impl/walletsecretstorage/WalletSecretEnvelope.kt — AES-GCM envelope (with unit tests: round-trip, wrong key, tampered ct, tampered iv, wrong AAD, wrong version, malformed JSON) - impl/walletsecretstorage/RecoveryKeyDerivation.kt — base58 decode + parity check + HKDF-SHA256 (with unit tests: determinism, whitespace tolerance, distinct info labels → distinct keys) - impl/walletsecretstorage/MatrixAccountDataWalletSecretStorage.kt — WalletSecretStorage impl wrapping Client account data - test/walletsecretstorage/FakeWalletSecretStorage.kt — in-memory fake - api/MatrixClient.kt: old .secretStorage → .walletSecretStorage - features/wallet/.../WalletBackupServiceImpl.kt — rewired to use the new interface; hasBackupWithoutKey now goes through the same path instead of manually poking the raw Matrix HTTP API. - DELETED: api/secretstorage/SecretStorage.kt, SecretStore.kt, impl/ secretstorage/RustSecretStorage.kt — the old SDK-dependent path. ### Backward compat note Users who backed up a wallet seed on the OLD SDK have a blob in Matrix's 4S at `com.sulkta.cardano.wallet_seed`. This branch cannot read those. Since the prior integration was only tested internally, acceptable today — anyone with an old backup re-enters their mnemonic.
This commit is contained in:
parent
a944499eda
commit
de2edafe61
13 changed files with 797 additions and 166 deletions
|
|
@ -42,7 +42,9 @@ import io.element.android.libraries.matrix.api.sync.SyncService
|
|||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.walletsecretstorage.WalletSecretStorage
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.walletsecretstorage.FakeWalletSecretStorage
|
||||
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService
|
||||
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
|
||||
|
|
@ -82,6 +84,7 @@ class FakeMatrixClient(
|
|||
override val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||
override val syncService: SyncService = FakeSyncService(),
|
||||
override val encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
override val walletSecretStorage: WalletSecretStorage = FakeWalletSecretStorage(),
|
||||
override val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(),
|
||||
override val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
|
||||
override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.test.walletsecretstorage
|
||||
|
||||
import io.element.android.libraries.matrix.api.walletsecretstorage.WalletSecretStorage
|
||||
|
||||
/**
|
||||
* In-memory fake for [WalletSecretStorage]. Stores the last put value as
|
||||
* plaintext keyed on the recovery key so tests can round-trip without
|
||||
* standing up real crypto / real account data.
|
||||
*/
|
||||
class FakeWalletSecretStorage : WalletSecretStorage {
|
||||
private val store: MutableMap<String, String> = mutableMapOf()
|
||||
var putSeedResult: (String, String) -> Result<Unit> = { recoveryKey, seed ->
|
||||
store[recoveryKey] = seed
|
||||
Result.success(Unit)
|
||||
}
|
||||
var getSeedResult: (String) -> Result<String?> = { recoveryKey ->
|
||||
Result.success(store[recoveryKey])
|
||||
}
|
||||
var hasSeedBackupResult: () -> Result<Boolean> = { Result.success(store.isNotEmpty()) }
|
||||
var deleteSeedResult: () -> Result<Unit> = {
|
||||
store.clear()
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun putSeed(recoveryKey: String, seedPhrase: String): Result<Unit> =
|
||||
putSeedResult(recoveryKey, seedPhrase)
|
||||
|
||||
override suspend fun getSeed(recoveryKey: String): Result<String?> =
|
||||
getSeedResult(recoveryKey)
|
||||
|
||||
override suspend fun hasSeedBackup(): Result<Boolean> = hasSeedBackupResult()
|
||||
|
||||
override suspend fun deleteSeed(): Result<Unit> = deleteSeedResult()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue