Skip to content

Commit

Permalink
Allow privileged wallets to query the backup status of a seed (#360)
Browse files Browse the repository at this point in the history
* WalletContractV1 changes to expose seed backup status to privileged apps

* Add support for Seed backup status to simulator

* Add support for Seed backup status to fakewallet

* Add support for Seed backup status to CTS tests
  • Loading branch information
sdlaver authored Nov 26, 2024
1 parent 776133f commit dd51dec
Show file tree
Hide file tree
Showing 16 changed files with 204 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class WalletContentProvider : ContentProvider() {
}

override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
enforcePermission()
enforceCallerPermission()
checkDependencyInjection()

return when (method) {
Expand Down Expand Up @@ -115,7 +115,7 @@ class WalletContentProvider : ContentProvider() {
queryArgs: Bundle?,
cancellationSignal: CancellationSignal?
): Cursor {
enforcePermission()
enforceCallerPermission()
checkDependencyInjection()

val match = uriMatcher.match(uri)
Expand Down Expand Up @@ -175,7 +175,12 @@ class WalletContentProvider : ContentProvider() {
projection: Array<out String>?,
queryArgs: Bundle?
): Cursor {
val defaultProjection = WalletContractV1.AUTHORIZED_SEEDS_ALL_COLUMNS.toSet()
val callerIsPrivileged =
callerHasPermission(WalletContractV1.PERMISSION_ACCESS_SEED_VAULT_PRIVILEGED)
// Only privileged wallets can retrieve the AUTHORIZED_SEEDS_IS_BACKED_UP column
val defaultProjection = WalletContractV1.AUTHORIZED_SEEDS_ALL_COLUMNS.filter { column ->
callerIsPrivileged || column != WalletContractV1.AUTHORIZED_SEEDS_IS_BACKED_UP
}.toSet()
val queryParser = makeQueryParser(defaultProjection, queryArgs)
val filteredProjection = projection?.intersect(defaultProjection) ?: defaultProjection
val cursor = MatrixCursor(filteredProjection.toTypedArray())
Expand All @@ -184,10 +189,9 @@ class WalletContentProvider : ContentProvider() {
runBlocking {
seedRepository.delayUntilDataValid()

if (requireContext().checkCallingPermission(WalletContractV1.PERMISSION_ACCESS_SEED_VAULT_PRIVILEGED) == PackageManager.PERMISSION_GRANTED) {
if (callerIsPrivileged) {
seedRepository.authorizeAllSeedsForUid(
uid,
Authorization.Purpose.SIGN_SOLANA_TRANSACTIONS
uid, Authorization.Purpose.SIGN_SOLANA_TRANSACTIONS
)
}
}
Expand All @@ -196,10 +200,11 @@ class WalletContentProvider : ContentProvider() {
seed.authorizations.forEach { auth ->
// Note: must be in the same order as defaultProjection
val values = arrayOf(
auth.authToken, // WalletContractV1.AUTHORIZED_SEEDS_AUTH_TOKEN
auth.purpose.toWalletContractConstant(), // WalletContractV1.AUTHORIZED_SEEDS_AUTH_PURPOSE
seed.details.name ?: "" // WalletContractV1.AUTHORIZED_SEEDS_SEED_NAME
)
auth.authToken, // WalletContractV1.AUTHORIZED_SEEDS_AUTH_TOKEN
auth.purpose.toWalletContractConstant(), // WalletContractV1.AUTHORIZED_SEEDS_AUTH_PURPOSE
seed.details.name ?: "", // WalletContractV1.AUTHORIZED_SEEDS_SEED_NAME
if (seed.details.isBackedUp) 1.toShort() else 0.toShort(), // WalletContractV1.AUTHORIZED_SEEDS_IS_BACKED_UP
).sliceArray(defaultProjection.indices)

if (auth.uid == uid
&& (authToken == null || auth.authToken == authToken)
Expand All @@ -222,6 +227,8 @@ class WalletContentProvider : ContentProvider() {
projection: Array<out String>?,
queryArgs: Bundle?
): Cursor {
val callerIsPrivileged =
callerHasPermission(WalletContractV1.PERMISSION_ACCESS_SEED_VAULT_PRIVILEGED)
val purposeAsEnum = purpose?.let { Authorization.Purpose.fromWalletContractConstant(it) }
val defaultProjection = WalletContractV1.UNAUTHORIZED_SEEDS_ALL_COLUMNS.toSet()
val queryParser = makeQueryParser(defaultProjection, queryArgs)
Expand All @@ -231,10 +238,9 @@ class WalletContentProvider : ContentProvider() {
val seedRepository = dependencyContainer.seedRepository
runBlocking {
seedRepository.delayUntilDataValid()
if (requireContext().checkCallingPermission(WalletContractV1.PERMISSION_ACCESS_SEED_VAULT_PRIVILEGED) == PackageManager.PERMISSION_GRANTED) {
if (callerIsPrivileged) {
seedRepository.authorizeAllSeedsForUid(
uid,
Authorization.Purpose.SIGN_SOLANA_TRANSACTIONS
uid, Authorization.Purpose.SIGN_SOLANA_TRANSACTIONS
)
}
}
Expand Down Expand Up @@ -378,7 +384,7 @@ class WalletContentProvider : ContentProvider() {
uri: Uri,
extras: Bundle?
): Int {
enforcePermission()
enforceCallerPermission()
checkDependencyInjection()

val match = uriMatcher.match(uri)
Expand Down Expand Up @@ -434,7 +440,7 @@ class WalletContentProvider : ContentProvider() {
values: ContentValues?,
extras: Bundle?
): Int {
enforcePermission()
enforceCallerPermission()
checkDependencyInjection()

val match = uriMatcher.match(uri)
Expand Down Expand Up @@ -599,9 +605,12 @@ class WalletContentProvider : ContentProvider() {
}
}

private fun enforcePermission() {
if (requireContext().checkCallingPermission(WalletContractV1.PERMISSION_ACCESS_SEED_VAULT) == PackageManager.PERMISSION_GRANTED ||
requireContext().checkCallingPermission(WalletContractV1.PERMISSION_ACCESS_SEED_VAULT_PRIVILEGED) == PackageManager.PERMISSION_GRANTED) {
private fun callerHasPermission(permission: String): Boolean =
requireContext().checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED

private fun enforceCallerPermission() {
if (callerHasPermission(WalletContractV1.PERMISSION_ACCESS_SEED_VAULT) ||
callerHasPermission(WalletContractV1.PERMISSION_ACCESS_SEED_VAULT_PRIVILEGED)) {
return
}
throw SecurityException("Permission Denial:: opening provider $TAG requires ${WalletContractV1.PERMISSION_ACCESS_SEED_VAULT} or ${WalletContractV1.PERMISSION_ACCESS_SEED_VAULT_PRIVILEGED}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ class SeedRepository @Inject constructor(
sr.seed.seedPhraseWordIndicesList,
sr.seed.name.ifEmpty { null },
sr.seed.pin,
sr.seed.unlockWithBiometrics
sr.seed.unlockWithBiometrics,
sr.seed.isBackedUp
)
val authorizations = sr.authorizationsList.map { ae ->
Authorization(ae.uid, ae.authToken, Authorization.Purpose.entries[ae.purpose])
Expand Down Expand Up @@ -532,6 +533,7 @@ class SeedRepository @Inject constructor(
}
pin = details.pin
unlockWithBiometrics = details.unlockWithBiometrics
isBackedUp = details.isBackedUp
}

// NOTE: should be called with mutex held
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class SeedDetails(
val name: String? = null,
val pin: String,
val unlockWithBiometrics: Boolean = false,
val isBackedUp: Boolean = false,
) {
companion object {
const val ENTROPY_SHORT = 128
Expand Down Expand Up @@ -45,6 +46,7 @@ data class SeedDetails(
if (name != other.name) return false
if (pin != other.pin) return false
if (unlockWithBiometrics != other.unlockWithBiometrics) return false
if (isBackedUp != other.isBackedUp) return false

return true
}
Expand All @@ -55,6 +57,7 @@ data class SeedDetails(
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + pin.hashCode()
result = 31 * result + unlockWithBiometrics.hashCode()
result = 31 * result + isBackedUp.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ class SeedDetailActivity : AppCompatActivity() {
}

Row(
modifier = Modifier.padding(vertical = Sizes.dp16),
modifier = Modifier.padding(top = Sizes.dp16),
verticalAlignment = Alignment.CenterVertically
) {
Text(
Expand All @@ -314,6 +314,31 @@ class SeedDetailActivity : AppCompatActivity() {
)
}

Row(
modifier = Modifier.padding(top = Sizes.dp4, bottom = Sizes.dp16),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.padding(end = Sizes.dp16)
.weight(1f),
text = stringResource(id = R.string.label_is_seed_backed_up),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Switch(
modifier = Modifier.semantics {
testTag = "IsBackedUp"
},
checked = seedDetails.isBackedUp,
onCheckedChange = {
viewModel.setSeedIsBackedUp(it)
}
)
}

Row(
modifier = Modifier
.fillMaxWidth()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class SeedDetailViewModel @Inject constructor(
},
pin = seed.details.pin,
enableBiometrics = seed.details.unlockWithBiometrics,
isBackedUp = seed.details.isBackedUp,
accounts = seed.accounts,
authorizedApps = seed.authorizations
)
Expand Down Expand Up @@ -109,6 +110,11 @@ class SeedDetailViewModel @Inject constructor(
_seedDetailUiState.update { it.copy(enableBiometrics = en) }
}

fun setSeedIsBackedUp(isBackedUp: Boolean) {
Log.d(TAG, "setSeedIsBackedUp($isBackedUp)")
_seedDetailUiState.update { it.copy(isBackedUp = isBackedUp) }
}

private fun setErrorMessage(message: String?) {
_seedDetailUiState.update { it.copy(errorMessage = message) }
}
Expand All @@ -126,7 +132,14 @@ class SeedDetailViewModel @Inject constructor(
Bip39PhraseUseCase.toIndex(w)
}
val seedBytes = Bip39PhraseUseCase.toSeed(phrase)
SeedDetails(seedBytes, phrase, it.name.ifBlank { null }, it.pin, it.enableBiometrics)
SeedDetails(
seedBytes,
phrase,
it.name.ifBlank { null },
it.pin,
it.enableBiometrics,
it.isBackedUp
)
}

Log.i(TAG, "Successfully created Seed $seedDetails; committing to SeedRepository")
Expand Down Expand Up @@ -209,6 +222,7 @@ data class SeedDetailUiState(
val phrase: List<String> = List(SeedPhraseLength.SEED_PHRASE_24_WORDS.length) { "" },
val pin: String = "",
val enableBiometrics: Boolean = false,
val isBackedUp: Boolean = false,
val accounts: List<Account> = listOf(),
val authorizedApps: List<Authorization> = listOf(),
val errorMessage: String? = null,
Expand Down
1 change: 1 addition & 0 deletions SeedVaultSimulator/src/main/proto/seed_repo.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ message SeedEntry {
string name = 3;
string pin = 4;
bool unlock_with_biometrics = 5;
bool is_backed_up = 6;
}

message AuthorizationEntry {
Expand Down
1 change: 1 addition & 0 deletions SeedVaultSimulator/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<string name="hint_pin">4 to 20 characters</string>
<string name="error_invalid_pin">PIN must be between 4 and 20 characters</string>
<string name="label_enable_biometrics_for_seed">Enable biometrics for this seed</string>
<string name="label_is_seed_backed_up">Mark seed as backed up</string>
<string name="label_authorized_apps">Authorized wallet apps</string>

<!-- Seed phrase word RecyclerView item strings -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2024 Solana Mobile Inc.
*/

package com.solanamobile.seedvault.cts

import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class PrivilegedSeedVaultChecker @Inject constructor() {
fun isPrivileged(): Boolean {
@Suppress("KotlinConstantConditions")
return BuildConfig.FLAVOR == "Privileged"
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
* Copyright (c) 2024 Solana Mobile Inc.
*/

package com.solanamobile.seedvault.cts.data

import javax.inject.Inject
Expand Down
Loading

0 comments on commit dd51dec

Please sign in to comment.