From cf408c7c574b121058fbcd5abde1d826238cd993 Mon Sep 17 00:00:00 2001 From: sdlaver <103003665+sdlaver@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:56:21 -0800 Subject: [PATCH] Ensure accounts are always derived for privileged apps (#370) --- .../contentprovider/WalletContentProvider.kt | 30 ++-- .../seedvaultimpl/data/SeedRepository.kt | 142 ++++++------------ .../ui/seeddetail/SeedDetailViewModel.kt | 15 +- .../fakewallet/ui/MainViewModel.kt | 13 ++ 4 files changed, 89 insertions(+), 111 deletions(-) diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/contentprovider/WalletContentProvider.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/contentprovider/WalletContentProvider.kt index 68874c17..e2fad9cd 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/contentprovider/WalletContentProvider.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/contentprovider/WalletContentProvider.kt @@ -549,20 +549,24 @@ class WalletContentProvider : ContentProvider() { SeedRepository.ChangeNotification.Type.CREATE -> listOf( WalletContractV1.UNAUTHORIZED_SEEDS_CONTENT_URI, - ContentUris.withAppendedId( - WalletContractV1.AUTHORIZED_SEEDS_CONTENT_URI, - change.id!!.toLong() - ) + change.id?.let { + ContentUris.withAppendedId( + WalletContractV1.AUTHORIZED_SEEDS_CONTENT_URI, + change.id.toLong() + ) + } ?: WalletContractV1.AUTHORIZED_SEEDS_CONTENT_URI ) SeedRepository.ChangeNotification.Type.UPDATE -> throw AssertionError("Authorizations are not expected to be updated") SeedRepository.ChangeNotification.Type.DELETE -> listOf( WalletContractV1.UNAUTHORIZED_SEEDS_CONTENT_URI, - ContentUris.withAppendedId( - WalletContractV1.AUTHORIZED_SEEDS_CONTENT_URI, - change.id!!.toLong() - ), + change.id?.let { + ContentUris.withAppendedId( + WalletContractV1.AUTHORIZED_SEEDS_CONTENT_URI, + change.id.toLong() + ) + } ?: WalletContractV1.AUTHORIZED_SEEDS_CONTENT_URI, WalletContractV1.ACCOUNTS_CONTENT_URI ) } @@ -572,10 +576,12 @@ class WalletContentProvider : ContentProvider() { SeedRepository.ChangeNotification.Type.CREATE, SeedRepository.ChangeNotification.Type.UPDATE -> listOf( - ContentUris.withAppendedId( - WalletContractV1.ACCOUNTS_CONTENT_URI, - change.id!!.toLong() - ) + change.id?.let { + ContentUris.withAppendedId( + WalletContractV1.ACCOUNTS_CONTENT_URI, + change.id.toLong() + ) + } ?: WalletContractV1.ACCOUNTS_CONTENT_URI ) SeedRepository.ChangeNotification.Type.DELETE -> throw AssertionError("Accounts are not expected to be deleted") diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/data/SeedRepository.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/data/SeedRepository.kt index 9dc54506..db0dcf75 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/data/SeedRepository.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/data/SeedRepository.kt @@ -162,8 +162,11 @@ class SeedRepository @Inject constructor( check(!seeds.value.containsKey(nextSeedId)) { "Seed repository already contains an entry for seed $nextSeedId" } newSeedRecordBuilder.seedId = nextSeedId id = nextSeedId + val changeNotification = ChangeNotification( + ChangeNotification.Category.SEED, ChangeNotification.Type.CREATE, id + ) - updateCompleteJob = updateSeedCollectionDataStore { + updateCompleteJob = updateSeedCollectionDataStore(changeNotification) { it.toBuilder().addSeeds(newSeedRecordBuilder).apply { nextId = ++nextSeedId }.build() @@ -171,14 +174,6 @@ class SeedRepository @Inject constructor( } updateCompleteJob.join() - - _changes.emit( - ChangeNotification( - ChangeNotification.Category.SEED, - ChangeNotification.Type.CREATE, - id - ) - ) } Log.d(TAG, "EXIT createSeed: $details -> $id") @@ -196,8 +191,11 @@ class SeedRepository @Inject constructor( val newSeedEntryBuilder = createSeedEntryBuilderFromSeed(details) val updateCompleteJob: Job + val changeNotification = ChangeNotification( + ChangeNotification.Category.SEED, ChangeNotification.Type.UPDATE, id + ) mutex.withLock { - updateCompleteJob = updateSeedCollectionDataStore { + updateCompleteJob = updateSeedCollectionDataStore(changeNotification) { val i = it.seedsList.indexOfFirst { seed -> seed.seedId == id } check(i != -1) { "Seed repository does not contain an entry for seed $id" } val newSeedRecordBuilder = @@ -207,14 +205,6 @@ class SeedRepository @Inject constructor( } updateCompleteJob.join() - - _changes.emit( - ChangeNotification( - ChangeNotification.Category.SEED, - ChangeNotification.Type.UPDATE, - id - ) - ) } Log.d(TAG, "EXIT updateSeed: $details") @@ -229,8 +219,11 @@ class SeedRepository @Inject constructor( // in the event of cancellation of the originating context. withContext(repositoryOwnerScope.coroutineContext) { val updateCompleteJob: Job + val changeNotification = ChangeNotification( + ChangeNotification.Category.SEED, ChangeNotification.Type.DELETE, id + ) mutex.withLock { - updateCompleteJob = updateSeedCollectionDataStore { + updateCompleteJob = updateSeedCollectionDataStore(changeNotification) { val i = it.seedsList.indexOfFirst { seed -> seed.seedId == id } require(i != -1) { "Seed repository does not contain an entry for seed $id" } it.toBuilder().removeSeeds(i).build() @@ -238,14 +231,6 @@ class SeedRepository @Inject constructor( } updateCompleteJob.join() - - _changes.emit( - ChangeNotification( - ChangeNotification.Category.SEED, - ChangeNotification.Type.DELETE, - id - ) - ) } Log.d(TAG, "EXIT deleteSeed: $id") @@ -260,21 +245,16 @@ class SeedRepository @Inject constructor( // in the event of cancellation of the originating context. withContext(repositoryOwnerScope.coroutineContext) { val updateCompleteJob: Job + val changeNotification = ChangeNotification( + ChangeNotification.Category.SEED, ChangeNotification.Type.DELETE, null + ) mutex.withLock { - updateCompleteJob = updateSeedCollectionDataStore { + updateCompleteJob = updateSeedCollectionDataStore(changeNotification) { it.toBuilder().clearSeeds().build() } } updateCompleteJob.join() - - _changes.emit( - ChangeNotification( - ChangeNotification.Category.SEED, - ChangeNotification.Type.DELETE, - null - ) - ) } Log.d(TAG, "EXIT deleteAllSeeds") @@ -301,8 +281,13 @@ class SeedRepository @Inject constructor( mutex.withLock { var assignedAuthToken = nextAuthToken newAuthorizationEntryBuilder.authToken = nextAuthToken + val changeNotification = ChangeNotification( + ChangeNotification.Category.AUTHORIZATION, + ChangeNotification.Type.CREATE, + assignedAuthToken + ) - updateCompleteJob = updateSeedCollectionDataStore { + updateCompleteJob = updateSeedCollectionDataStore(changeNotification) { val i = it.seedsList.indexOfFirst { sr -> sr.seedId == id } require(i != -1) { "Seed repository does not contain an entry for seed $id" } val existingAuthRecord = @@ -323,14 +308,6 @@ class SeedRepository @Inject constructor( } updateCompleteJob.join() - - _changes.emit( - ChangeNotification( - ChangeNotification.Category.AUTHORIZATION, - ChangeNotification.Type.CREATE, - authToken - ) - ) } Log.d(TAG, "EXIT authorizeSeedForUid: $id/$uid -> $authToken") @@ -346,9 +323,11 @@ class SeedRepository @Inject constructor( // in the event of cancellation of the originating context. withContext(repositoryOwnerScope.coroutineContext) { val updateAllSeedsJob: Job - val approvedAuthTokens = mutableListOf() + val changeNotification = ChangeNotification( + ChangeNotification.Category.AUTHORIZATION, ChangeNotification.Type.CREATE, null + ) mutex.withLock { - updateAllSeedsJob = updateSeedCollectionDataStore { seedCollection -> + updateAllSeedsJob = updateSeedCollectionDataStore(changeNotification) { seedCollection -> val newSeedCollection = seedCollection.toBuilder() seedCollection.seedsList.forEachIndexed { index, seedRecord -> val existingAuthRecord = @@ -360,7 +339,6 @@ class SeedRepository @Inject constructor( this.purpose = purpose.ordinal this.authToken = nextAuthToken++ } - approvedAuthTokens.add(newAuthorizationEntryBuilder.authToken) val newSeedRecordBuilder = seedRecord.toBuilder() .addAuthorizations(newAuthorizationEntryBuilder) newSeedRecordBuilder.build() @@ -375,15 +353,6 @@ class SeedRepository @Inject constructor( } } updateAllSeedsJob.join() - approvedAuthTokens.forEach { - _changes.emit( - ChangeNotification( - ChangeNotification.Category.AUTHORIZATION, - ChangeNotification.Type.CREATE, - it - ) - ) - } } Log.d(TAG, "EXIT authorizeAllSeedsForUid") } @@ -397,8 +366,11 @@ class SeedRepository @Inject constructor( // in the event of cancellation of the originating context. withContext(repositoryOwnerScope.coroutineContext) { val updateCompleteJob: Job + val changeNotification = ChangeNotification( + ChangeNotification.Category.AUTHORIZATION, ChangeNotification.Type.DELETE, authToken + ) mutex.withLock { - updateCompleteJob = updateSeedCollectionDataStore { + updateCompleteJob = updateSeedCollectionDataStore(changeNotification) { val i = it.seedsList.indexOfFirst { sr -> sr.seedId == id } require(i != -1) { "Seed repository does not contain an entry for seed $id" } val j = @@ -410,14 +382,6 @@ class SeedRepository @Inject constructor( } updateCompleteJob.join() - - _changes.emit( - ChangeNotification( - ChangeNotification.Category.AUTHORIZATION, - ChangeNotification.Type.DELETE, - authToken - ) - ) } Log.d(TAG, "EXIT deauthorizeSeed: $id/$authToken") @@ -450,8 +414,13 @@ class SeedRepository @Inject constructor( mutex.withLock { var assignedAccountId = nextAccountId newKnownAccountEntryBuilder.accountId = nextAccountId + val changeNotification = ChangeNotification( + ChangeNotification.Category.ACCOUNT, + ChangeNotification.Type.CREATE, + assignedAccountId + ) - updateCompleteJob = updateSeedCollectionDataStore { + updateCompleteJob = updateSeedCollectionDataStore(changeNotification) { val i = it.seedsList.indexOfFirst { sr -> sr.seedId == id } require(i != -1) { "Seed repository does not contain an entry for seed $id" } val bip32Uri = account.bip32DerivationPathUri.toString() @@ -475,14 +444,6 @@ class SeedRepository @Inject constructor( } updateCompleteJob.join() - - _changes.emit( - ChangeNotification( - ChangeNotification.Category.ACCOUNT, - ChangeNotification.Type.CREATE, - accountId - ) - ) } Log.d(TAG, "EXIT addKnownAccountForSeed") @@ -499,8 +460,11 @@ class SeedRepository @Inject constructor( // in the event of cancellation of the originating context. withContext(repositoryOwnerScope.coroutineContext) { val updateCompleteJob: Job + val changeNotification = ChangeNotification( + ChangeNotification.Category.ACCOUNT, ChangeNotification.Type.DELETE, null + ) mutex.withLock { - updateCompleteJob = updateSeedCollectionDataStore { + updateCompleteJob = updateSeedCollectionDataStore(changeNotification) { val i = it.seedsList.indexOfFirst { sr -> sr.seedId == id } require(i != -1) { "Seed repository does not contain an entry for seed $id" } val newSeedRecordBuilder = @@ -510,13 +474,6 @@ class SeedRepository @Inject constructor( } updateCompleteJob.join() - _changes.emit( - ChangeNotification( - ChangeNotification.Category.ACCOUNT, - ChangeNotification.Type.DELETE, - null - ) - ) } Log.d(TAG, "EXIT removeAllKnownAccountForSeed") @@ -544,8 +501,11 @@ class SeedRepository @Inject constructor( } val updateCompleteJob: Job + val changeNotification = ChangeNotification( + ChangeNotification.Category.ACCOUNT, ChangeNotification.Type.UPDATE, account.id + ) mutex.withLock { - updateCompleteJob = updateSeedCollectionDataStore { + updateCompleteJob = updateSeedCollectionDataStore(changeNotification) { val i = it.seedsList.indexOfFirst { sr -> sr.seedId == id } require(i != -1) { "Seed repository does not contain an entry for seed $id" } val j = @@ -558,14 +518,6 @@ class SeedRepository @Inject constructor( } updateCompleteJob.join() - - _changes.emit( - ChangeNotification( - ChangeNotification.Category.ACCOUNT, - ChangeNotification.Type.UPDATE, - account.id - ) - ) } Log.d(TAG, "EXIT updateKnownAccountForSeed") @@ -583,12 +535,16 @@ class SeedRepository @Inject constructor( } // NOTE: should be called with mutex held - private suspend fun updateSeedCollectionDataStore(transform: suspend (t: SeedCollection) -> SeedCollection): Job { + private suspend fun updateSeedCollectionDataStore( + changeNotification: ChangeNotification, + transform: suspend (t: SeedCollection) -> SeedCollection + ): Job { // Launch undispatched, so that the first element of seedCollection (which is a replay of // the last emitted value) is collected immediately. val updateCompleteJob = repositoryOwnerScope.launch(start = CoroutineStart.UNDISPATCHED) { withTimeout(CHANGE_PROPAGATION_TIMEOUT_MS) { seedCollection.take(2).collect() + _changes.emit(changeNotification) } } diff --git a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/seeddetail/SeedDetailViewModel.kt b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/seeddetail/SeedDetailViewModel.kt index 139573b3..a50a2871 100644 --- a/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/seeddetail/SeedDetailViewModel.kt +++ b/SeedVaultSimulator/src/main/java/com/solanamobile/seedvaultimpl/ui/seeddetail/SeedDetailViewModel.kt @@ -133,14 +133,17 @@ class SeedDetailViewModel @Inject constructor( when (val mode = mode) { // immutable snapshot of mode is NewSeedMode -> { val seedId = seedRepository.createSeed(seedDetails) - mode.authorize?.let { authorize -> - val authToken = seedRepository.authorizeSeedForUid(seedId, authorize.uid, authorize.purpose) - // Ensure that the seed vault contains appropriate known accounts for this authorization purpose - val seed = seedRepository.seeds.value[seedId]!! - PrepopulateKnownAccountsUseCase(seedRepository).populateKnownAccounts(seed, authorize.purpose) + // Pre-populate known accounts for all purposes + val seed = seedRepository.seeds.value[seedId]!! + PrepopulateKnownAccountsUseCase(seedRepository).apply { + for (purpose in Authorization.Purpose.entries) { + populateKnownAccounts(seed, purpose) + } + } - authToken + mode.authorize?.let { authorize -> + seedRepository.authorizeSeedForUid(seedId, authorize.uid, authorize.purpose) } ?: -1L } is EditSeedMode -> { diff --git a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainViewModel.kt b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainViewModel.kt index 66ad079b..34d512fb 100644 --- a/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainViewModel.kt +++ b/fakewallet/src/main/java/com/solanamobile/fakewallet/ui/MainViewModel.kt @@ -14,6 +14,7 @@ import android.os.Parcelable import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.solanamobile.fakewallet.BuildConfig import com.solanamobile.fakewallet.usecase.VerifyEd25519SignatureUseCase import com.solanamobile.seedvault.* import kotlinx.coroutines.Dispatchers @@ -44,6 +45,14 @@ class MainViewModel( viewModelScope.launch { observeSeedVaultContentChanges() refreshUiState() + + // Privileged wallets do not manually authorize individual seeds, so check on startup + // that accounts have been marked as user wallets. + if (BuildConfig.FLAVOR == "Privileged") { + _uiState.value.seeds.forEach { seed -> + markAccountsAsUserWallets(seed.authToken) + } + } } } @@ -169,6 +178,10 @@ class MainViewModel( @Suppress("UNUSED_PARAMETER") fun onAddSeedSuccess(event: ViewModelEvent.AddSeedViewModelEvent, authToken: Long) { + markAccountsAsUserWallets(authToken) + } + + private fun markAccountsAsUserWallets(authToken: Long) { // Mark two accounts as user wallets. This simulates a real wallet app exploring each // account and marking them as containing user funds. viewModelScope.launch {