Prevent 4S / megolm backup desync + sign with MSK

This commit is contained in:
Valere 2022-05-04 14:16:12 +02:00
parent 3f8ddbec60
commit 22e0506814
16 changed files with 524 additions and 182 deletions

1
changelog.d/5906.bugfix Normal file
View File

@ -0,0 +1 @@
Desynchronized 4S | Megolm backup causing Unusable backup

View File

@ -16,25 +16,35 @@
package org.matrix.android.sdk.api.session.crypto.keysbackup
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
/**
* A signature in a `KeysBackupVersionTrust` object.
*/
data class KeysBackupVersionTrustSignature(
/**
* The id of the device that signed the backup version.
*/
val deviceId: String?,
/**
* The device that signed the backup version.
* Can be null if the device is not known.
*/
val device: CryptoDeviceInfo?,
sealed class KeysBackupVersionTrustSignature {
/**
* Flag to indicate the signature from this device is valid.
*/
val valid: Boolean,
)
data class DeviceSignature(
/**
* The id of the device that signed the backup version.
*/
val deviceId: String?,
/**
* The device that signed the backup version.
* Can be null if the device is not known.
*/
val device: CryptoDeviceInfo?,
/**
* Flag to indicate the signature from this device is valid.
*/
val valid: Boolean) : KeysBackupVersionTrustSignature()
data class UserSignature(
val keyId: String?,
val cryptoCrossSigningKey: CryptoCrossSigningKey?,
val valid: Boolean
) : KeysBackupVersionTrustSignature()
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
import javax.inject.Inject
/**
* Holds the OlmPkSigning for cross signing.
* Can be injected without having to get the full cross signing service
*/
@SessionScope
internal class CrossSigningOlm @Inject constructor(
private val cryptoStore: IMXCryptoStore,
) {
enum class KeyType {
SELF,
USER,
MASTER
}
var olmUtility: OlmUtility = OlmUtility()
var masterPkSigning: OlmPkSigning? = null
var userPkSigning: OlmPkSigning? = null
var selfSigningPkSigning: OlmPkSigning? = null
fun release() {
olmUtility.releaseUtility()
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
}
fun signObject(type: KeyType, strToSign: String): Map<String, String> {
val myKeys = cryptoStore.getMyCrossSigningInfo()
val pubKey = when (type) {
KeyType.SELF -> myKeys?.selfSigningKey()
KeyType.USER -> myKeys?.userKey()
KeyType.MASTER -> myKeys?.masterKey()
}?.unpaddedBase64PublicKey
val pkSigning = when (type) {
KeyType.SELF -> selfSigningPkSigning
KeyType.USER -> userPkSigning
KeyType.MASTER -> masterPkSigning
}
if (pubKey == null || pkSigning == null) {
throw Throwable("Cannot sign from this account, public and/or privateKey Unknown $type|$pkSigning")
}
val signature = pkSigning.sign(strToSign)
return mapOf(
"ed25519:$pubKey" to signature
)
}
fun verifySignature(type: KeyType, signable: JsonDict, signatures: Map<String, Map<String, String>>) {
val myKeys = cryptoStore.getMyCrossSigningInfo()
?: throw NoSuchElementException("Cross Signing not configured")
val myUserID = myKeys.userId
val pubKey = when (type) {
KeyType.SELF -> myKeys.selfSigningKey()
KeyType.USER -> myKeys.userKey()
KeyType.MASTER -> myKeys.masterKey()
}?.unpaddedBase64PublicKey ?: throw NoSuchElementException("Cross Signing not configured")
val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me
?.get("ed25519:$pubKey")
if (signaturesMadeByMyKey.isNullOrBlank()) {
throw IllegalArgumentException("Not signed with my key $type")
}
// Check that Alice USK signature of Bob MSK is valid
olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable))
}
}

View File

@ -54,7 +54,6 @@ import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -72,19 +71,13 @@ internal class DefaultCrossSigningService @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val crossSigningOlm: CrossSigningOlm,
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
) : CrossSigningService,
DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null
private var masterPkSigning: OlmPkSigning? = null
private var userPkSigning: OlmPkSigning? = null
private var selfSigningPkSigning: OlmPkSigning? = null
init {
try {
olmUtility = OlmUtility()
// Try to get stored keys if they exist
cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo ->
@ -97,7 +90,7 @@ internal class DefaultCrossSigningService @Inject constructor(
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning = pkSigning
crossSigningOlm.masterPkSigning = pkSigning
Timber.i("## CrossSigning - Loading master key success")
} else {
Timber.w("## CrossSigning - Public master key does not match the private key")
@ -110,7 +103,7 @@ internal class DefaultCrossSigningService @Inject constructor(
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning = pkSigning
crossSigningOlm.userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading User Signing key success")
} else {
Timber.w("## CrossSigning - Public User key does not match the private key")
@ -123,7 +116,7 @@ internal class DefaultCrossSigningService @Inject constructor(
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning = pkSigning
crossSigningOlm.selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading Self Signing key success")
} else {
Timber.w("## CrossSigning - Public Self Signing key does not match the private key")
@ -145,8 +138,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
fun release() {
olmUtility?.releaseUtility()
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
crossSigningOlm.release()
deviceListManager.removeListener(this)
}
@ -179,9 +171,9 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
setUserKeysAsTrusted(userId, true)
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
crossSigningOlm.selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
callback.onSuccess(Unit)
}
@ -200,8 +192,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning?.releaseSigning()
masterPkSigning = pkSigning
crossSigningOlm.masterPkSigning?.releaseSigning()
crossSigningOlm.masterPkSigning = pkSigning
Timber.i("## CrossSigning - Loading MSK success")
cryptoStore.storeMSKPrivateKey(mskPrivateKey)
return
@ -227,8 +219,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning?.releaseSigning()
selfSigningPkSigning = pkSigning
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
crossSigningOlm.selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading SSK success")
cryptoStore.storeSSKPrivateKey(sskPrivateKey)
return
@ -254,8 +246,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning?.releaseSigning()
userPkSigning = pkSigning
crossSigningOlm.userPkSigning?.releaseSigning()
crossSigningOlm.userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading USK success")
cryptoStore.storeUSKPrivateKey(uskPrivateKey)
return
@ -284,8 +276,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning?.releaseSigning()
masterPkSigning = pkSigning
crossSigningOlm.masterPkSigning?.releaseSigning()
crossSigningOlm.masterPkSigning = pkSigning
masterKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
@ -301,8 +293,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning?.releaseSigning()
userPkSigning = pkSigning
crossSigningOlm.userPkSigning?.releaseSigning()
crossSigningOlm.userPkSigning = pkSigning
userKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
@ -318,8 +310,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning?.releaseSigning()
selfSigningPkSigning = pkSigning
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
crossSigningOlm.selfSigningPkSigning = pkSigning
selfSignedKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
@ -407,7 +399,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check that Alice USK signature of Bob MSK is valid
try {
olmUtility!!.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey, myUserKey.unpaddedBase64PublicKey, otherMasterKey.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
masterKeySignaturesMadeByMyUserKey,
myUserKey.unpaddedBase64PublicKey,
otherMasterKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
}
@ -461,7 +457,7 @@ internal class DefaultCrossSigningService @Inject constructor(
if (potentialDevice != null && potentialDevice.isVerified) {
// Check signature validity?
try {
olmUtility?.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
isMaterKeyTrusted = true
return@forEach
} catch (failure: Throwable) {
@ -490,7 +486,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check that Alice USK signature of Alice MSK is valid
try {
olmUtility!!.verifyEd25519Signature(userKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, myUserKey.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
userKeySignaturesMadeByMyMasterKey,
myMasterKey.unpaddedBase64PublicKey,
myUserKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
}
@ -509,7 +509,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check that Alice USK signature of Alice MSK is valid
try {
olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
ssKeySignaturesMadeByMyMasterKey,
myMasterKey.unpaddedBase64PublicKey,
mySSKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
}
@ -562,7 +566,7 @@ internal class DefaultCrossSigningService @Inject constructor(
return@launch
}
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
if (userPubKey == null || userPkSigning == null) {
if (userPubKey == null || crossSigningOlm.userPkSigning == null) {
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
return@launch
}
@ -571,7 +575,7 @@ internal class DefaultCrossSigningService @Inject constructor(
val newSignature = JsonCanonicalizer.getCanonicalJson(
Map::class.java,
otherMasterKeys.signalableJSONDictionary()
).let { userPkSigning?.sign(it) }
).let { crossSigningOlm.userPkSigning?.sign(it) }
if (newSignature == null) {
// race??
@ -618,13 +622,13 @@ internal class DefaultCrossSigningService @Inject constructor(
}
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
if (ssPubKey == null || selfSigningPkSigning == null) {
if (ssPubKey == null || crossSigningOlm.selfSigningPkSigning == null) {
callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey"))
return@launch
}
// Sign with self signing
val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable())
val newSignature = crossSigningOlm.selfSigningPkSigning?.sign(device.canonicalSignable())
if (newSignature == null) {
// race??
@ -697,7 +701,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check bob's device is signed by bob's SSK
try {
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
otherSSKSignature,
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
otherDevice.canonicalSignable()
)
} catch (e: Throwable) {
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e))
}
@ -747,7 +755,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check bob's device is signed by bob's SSK
try {
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
otherSSKSignature,
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
otherDevice.canonicalSignable()
)
} catch (e: Throwable) {
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
}

View File

@ -54,6 +54,7 @@ import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.android.sdk.internal.crypto.ObjectSigner
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm
import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
@ -102,6 +103,7 @@ internal class DefaultKeysBackupService @Inject constructor(
private val cryptoStore: IMXCryptoStore,
private val olmDevice: MXOlmDevice,
private val objectSigner: ObjectSigner,
private val crossSigningOlm: CrossSigningOlm,
// Actions
private val megolmSessionDataImporter: MegolmSessionDataImporter,
// Tasks
@ -178,7 +180,6 @@ internal class DefaultKeysBackupService @Inject constructor(
}
}
}
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
SignalableMegolmBackupAuthData(
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
@ -187,7 +188,6 @@ internal class DefaultKeysBackupService @Inject constructor(
)
} else {
val publicKey = olmPkDecryption.generateKey()
SignalableMegolmBackupAuthData(
publicKey = publicKey
)
@ -195,13 +195,28 @@ internal class DefaultKeysBackupService @Inject constructor(
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
val signatures = mutableMapOf<String, MutableMap<String, String>>()
val deviceSignature = objectSigner.signObject(canonicalJson)
deviceSignature.forEach { (userID, content) ->
signatures[userID] = content.toMutableMap()
}
// If we have cross signing add signature, will throw if cross signing not properly configured
try {
val crossSign = crossSigningOlm.signObject(CrossSigningOlm.KeyType.MASTER, canonicalJson)
signatures[credentials.userId]?.putAll(crossSign)
} catch (failure: Throwable) {
// ignore and log
Timber.w(failure, "prepareKeysBackupVersion: failed to sign with cross signing keys")
}
val signedMegolmBackupAuthData = MegolmBackupAuthData(
publicKey = signalableMegolmBackupAuthData.publicKey,
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
signatures = objectSigner.signObject(canonicalJson)
signatures = signatures
)
val creationInfo = MegolmBackupCreationInfo(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
authData = signedMegolmBackupAuthData,
@ -420,18 +435,41 @@ internal class DefaultKeysBackupService @Inject constructor(
for ((keyId, mySignature) in mySigs) {
// XXX: is this how we're supposed to get the device id?
var deviceId: String? = null
var deviceOrCrossSigningKeyId: String? = null
val components = keyId.split(":")
if (components.size == 2) {
deviceId = components[1]
deviceOrCrossSigningKeyId = components[1]
}
if (deviceId != null) {
val device = cryptoStore.getUserDevice(userId, deviceId)
// Let's check if it's my master key
val myMSKPKey = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.unpaddedBase64PublicKey
if (deviceOrCrossSigningKeyId == myMSKPKey) {
// we have to check if we can trust
var isSignatureValid = false
try {
crossSigningOlm.verifySignature(CrossSigningOlm.KeyType.MASTER, authData.signalableJSONDictionary(), authData.signatures)
isSignatureValid = true
} catch (failure: Throwable) {
Timber.w(failure, "getKeysBackupTrust: Bad signature from my user MSK")
}
val mskTrusted = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.isVerified() == true
if (isSignatureValid && mskTrusted) {
keysBackupVersionTrustIsUsable = true
}
val signature = KeysBackupVersionTrustSignature.UserSignature(
keyId = deviceOrCrossSigningKeyId,
cryptoCrossSigningKey = cryptoStore.getMyCrossSigningInfo()?.masterKey(),
valid = isSignatureValid
)
keysBackupVersionTrustSignatures.add(signature)
} else if (deviceOrCrossSigningKeyId != null) {
val device = cryptoStore.getUserDevice(userId, deviceOrCrossSigningKeyId)
var isSignatureValid = false
if (device == null) {
Timber.v("getKeysBackupTrust: Signature from unknown device $deviceId")
Timber.v("getKeysBackupTrust: Signature from unknown device $deviceOrCrossSigningKeyId")
} else {
val fingerprint = device.fingerprint()
if (fingerprint != null) {
@ -448,8 +486,8 @@ internal class DefaultKeysBackupService @Inject constructor(
}
}
val signature = KeysBackupVersionTrustSignature(
deviceId = deviceId,
val signature = KeysBackupVersionTrustSignature.DeviceSignature(
deviceId = deviceOrCrossSigningKeyId,
device = device,
valid = isSignatureValid,
)

View File

@ -128,7 +128,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
}
private fun launch4SActivity() {
SharedSecureStorageActivity.newIntent(
SharedSecureStorageActivity.newReadIntent(
context = this,
keyId = null, // default key
requestedSecrets = listOf(KEYBACKUP_SECRET_SSSS_NAME),

View File

@ -22,4 +22,8 @@ sealed class KeyBackupSettingsAction : VectorViewModelAction {
object Init : KeyBackupSettingsAction()
object GetKeyBackupTrust : KeyBackupSettingsAction()
object DeleteKeyBackup : KeyBackupSettingsAction()
object SetUpKeyBackup : KeyBackupSettingsAction()
data class StoreIn4SSuccess(val recoveryKey: String, val alias: String) : KeyBackupSettingsAction()
object StoreIn4SReset : KeyBackupSettingsAction()
object StoreIn4SFailure : KeyBackupSettingsAction()
}

View File

@ -15,6 +15,7 @@
*/
package im.vector.app.features.crypto.keysbackup.settings
import android.app.Activity
import android.content.Context
import android.content.Intent
import com.airbnb.mvrx.Fail
@ -23,9 +24,13 @@ import com.airbnb.mvrx.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
@AndroidEntryPoint
class KeysBackupManageActivity : SimpleFragmentActivity() {
@ -41,6 +46,21 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
private val viewModel: KeysBackupSettingsViewModel by viewModel()
private val secretStartForActivityResult = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
val result = activityResult.data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)
val reset = activityResult.data?.getBooleanExtra(SharedSecureStorageActivity.EXTRA_DATA_RESET, false) ?: false
if (result != null) {
viewModel.handle(KeyBackupSettingsAction.StoreIn4SSuccess(result, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
} else if (reset) {
// all have been reset so a new backup would have been created
viewModel.handle(KeyBackupSettingsAction.StoreIn4SReset)
}
} else {
viewModel.handle(KeyBackupSettingsAction.StoreIn4SFailure)
}
}
override fun initUiAndData() {
super.initUiAndData()
if (supportFragmentManager.fragments.isEmpty()) {
@ -69,6 +89,23 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
}
}
}
viewModel.observeViewEvents {
when (it) {
KeysBackupViewEvents.OpenLegacyCreateBackup -> {
startActivity(KeysBackupSetupActivity.intent(this, false))
}
is KeysBackupViewEvents.RequestStore4SSecret -> {
secretStartForActivityResult.launch(
SharedSecureStorageActivity.newWriteIntent(
this,
null, // default key
listOf(KEYBACKUP_SECRET_SSSS_NAME to it.recoveryKey)
)
)
}
}
}
}
override fun onBackPressed() {

View File

@ -28,7 +28,6 @@ import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentKeysBackupSettingsBinding
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import javax.inject.Inject
class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSettingsRecyclerViewController: KeysBackupSettingsRecyclerViewController) :
@ -58,9 +57,7 @@ class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSetti
}
override fun didSelectSetupMessageRecovery() {
context?.let {
startActivity(KeysBackupSetupActivity.intent(it, false))
}
viewModel.handle(KeyBackupSettingsAction.SetUpKeyBackup)
}
override fun didSelectRestoreMessageRecovery() {

View File

@ -29,9 +29,11 @@ import im.vector.app.core.ui.list.ItemStyle
import im.vector.app.core.ui.list.genericItem
import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature
import java.util.UUID
import javax.inject.Inject
@ -191,69 +193,105 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
}
}
is Success -> {
keysVersionTrust().signatures.forEach {
genericItem {
id(UUID.randomUUID().toString())
title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence())
val isDeviceKnown = it.device != null
val isDeviceVerified = it.device?.isVerified ?: false
val isSignatureValid = it.valid
val deviceId: String = it.deviceId ?: ""
if (!isDeviceKnown) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_warning)
} else {
if (isSignatureValid) {
if (host.session.sessionParams.deviceId == it.deviceId) {
keysVersionTrust()
.signatures
.filterIsInstance<KeysBackupVersionTrustSignature.UserSignature>()
.forEach {
val isUserVerified = it.cryptoCrossSigningKey?.trustLevel?.isVerified().orFalse()
val isSignatureValid = it.valid
val userId: String = it.cryptoCrossSigningKey?.userId ?: ""
if (userId == session.sessionParams.userId && isSignatureValid && isUserVerified) {
genericItem {
id(UUID.randomUUID().toString())
title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence())
description(
host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_this_device)
.getString(R.string.keys_backup_settings_signature_from_this_user)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_verified)
} else {
if (isDeviceVerified) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_verified)
} else {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_warning)
}
}
} else {
// Invalid signature
endIconResourceId(R.drawable.e2e_warning)
if (isDeviceVerified) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)
.toEpoxyCharSequence()
)
} else {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)
.toEpoxyCharSequence()
)
}
}
}
}
} // end for each
keysVersionTrust()
.signatures
.filterIsInstance<KeysBackupVersionTrustSignature.DeviceSignature>()
.forEach {
genericItem {
id(UUID.randomUUID().toString())
title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence())
val isDeviceKnown = it.device != null
val isDeviceVerified = it.device?.isVerified ?: false
val isSignatureValid = it.valid
val deviceId: String = it.deviceId ?: ""
if (!isDeviceKnown) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_warning)
} else {
if (isSignatureValid) {
if (host.session.sessionParams.deviceId == it.deviceId) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_this_device)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_verified)
} else {
if (isDeviceVerified) {
description(
host.stringProvider
.getString(
R.string.keys_backup_settings_valid_signature_from_verified_device,
deviceId
)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_verified)
} else {
description(
host.stringProvider
.getString(
R.string.keys_backup_settings_valid_signature_from_unverified_device,
deviceId
)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_warning)
}
}
} else {
// Invalid signature
endIconResourceId(R.drawable.e2e_warning)
if (isDeviceVerified) {
description(
host.stringProvider
.getString(
R.string.keys_backup_settings_invalid_signature_from_verified_device,
deviceId
)
.toEpoxyCharSequence()
)
} else {
description(
host.stringProvider
.getString(
R.string.keys_backup_settings_invalid_signature_from_unverified_device,
deviceId
)
.toEpoxyCharSequence()
)
}
}
}
}
} // end for each
}
is Fail -> {
errorWithRetryItem {

View File

@ -25,8 +25,8 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.session.Session
@ -34,10 +34,16 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.toBase64NoPadding
import timber.log.Timber
class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialState: KeysBackupSettingViewState,
session: Session
) : VectorViewModel<KeysBackupSettingViewState, KeyBackupSettingsAction, EmptyViewEvents>(initialState),
private val session: Session
) : VectorViewModel<KeysBackupSettingViewState, KeyBackupSettingsAction, KeysBackupViewEvents>(initialState),
KeysBackupStateListener {
@AssistedFactory
@ -49,6 +55,8 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
private val keysBackupService: KeysBackupService = session.cryptoService().keysBackupService()
var pendingBackupCreationInfo: MegolmBackupCreationInfo? = null
init {
setState {
this.copy(
@ -62,9 +70,18 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
override fun handle(action: KeyBackupSettingsAction) {
when (action) {
KeyBackupSettingsAction.Init -> init()
KeyBackupSettingsAction.GetKeyBackupTrust -> getKeysBackupTrust()
KeyBackupSettingsAction.DeleteKeyBackup -> deleteCurrentBackup()
KeyBackupSettingsAction.Init -> init()
KeyBackupSettingsAction.GetKeyBackupTrust -> getKeysBackupTrust()
KeyBackupSettingsAction.DeleteKeyBackup -> deleteCurrentBackup()
KeyBackupSettingsAction.SetUpKeyBackup -> viewModelScope.launch {
setUpKeyBackup()
}
KeyBackupSettingsAction.StoreIn4SReset,
KeyBackupSettingsAction.StoreIn4SFailure -> {
pendingBackupCreationInfo = null
// nothing to do just stay on fragment
}
is KeyBackupSettingsAction.StoreIn4SSuccess -> viewModelScope.launch { completeBackupCreation() }
}
}
@ -120,6 +137,35 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
getKeysBackupTrust()
}
suspend fun setUpKeyBackup() {
// We need to check if 4S is enabled first.
// If it is we need to use it, generate a random key
// for the backup and store it in the 4S
if (session.sharedSecretStorageService().isRecoverySetup()) {
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
pendingBackupCreationInfo = creationInfo
val recoveryKey = extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()
_viewEvents.post(KeysBackupViewEvents.RequestStore4SSecret(recoveryKey!!))
} else {
// No 4S so we can open legacy flow
_viewEvents.post(KeysBackupViewEvents.OpenLegacyCreateBackup)
}
}
suspend fun completeBackupCreation() {
val info = pendingBackupCreationInfo ?: return
val version = awaitCallback<KeysVersion> {
session.cryptoService().keysBackupService().createKeysBackupVersion(info, it)
}
// Save it for gossiping
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping")
session.cryptoService().keysBackupService().saveBackupRecoveryKey(info.recoveryKey, version = version.version)
// TODO catch, delete 4S account data
}
private fun deleteCurrentBackup() {
val keysBackupService = keysBackupService

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.crypto.keysbackup.settings
import im.vector.app.core.platform.VectorViewEvents
sealed class KeysBackupViewEvents : VectorViewEvents {
object OpenLegacyCreateBackup : KeysBackupViewEvents()
data class RequestStore4SSecret(val recoveryKey: String) : KeysBackupViewEvents()
}

View File

@ -48,8 +48,9 @@ class SharedSecureStorageActivity :
@Parcelize
data class Args(
val keyId: String?,
val requestedSecrets: List<String>,
val resultKeyStoreAlias: String
val requestedSecrets: List<String> = emptyList(),
val resultKeyStoreAlias: String,
val writeSecrets: List<Pair<String, String>> = emptyList(),
) : Parcelable
private val viewModel: SharedSecureStorageViewModel by viewModel()
@ -148,18 +149,36 @@ class SharedSecureStorageActivity :
const val EXTRA_DATA_RESET = "EXTRA_DATA_RESET"
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
fun newIntent(context: Context,
keyId: String? = null,
requestedSecrets: List<String>,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
fun newReadIntent(context: Context,
keyId: String? = null,
requestedSecrets: List<String>,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
require(requestedSecrets.isNotEmpty())
return Intent(context, SharedSecureStorageActivity::class.java).also {
it.putExtra(
Mavericks.KEY_ARG, Args(
keyId,
requestedSecrets,
resultKeyStoreAlias
Mavericks.KEY_ARG,
Args(
keyId = keyId,
requestedSecrets = requestedSecrets,
resultKeyStoreAlias = resultKeyStoreAlias
)
)
}
}
fun newWriteIntent(context: Context,
keyId: String? = null,
writeSecrets: List<Pair<String, String>>,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
require(writeSecrets.isNotEmpty())
return Intent(context, SharedSecureStorageActivity::class.java).also {
it.putExtra(
Mavericks.KEY_ARG,
Args(
keyId = keyId,
writeSecrets = writeSecrets,
resultKeyStoreAlias = resultKeyStoreAlias
)
)
}
}

View File

@ -39,13 +39,20 @@ import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
import org.matrix.android.sdk.api.session.securestorage.KeyInfo
import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.flow.flow
import timber.log.Timber
import java.io.ByteArrayOutputStream
sealed class RequestType {
data class ReadSecrets(val secretsName: List<String>) : RequestType()
data class WriteSecrets(val secretsNameValue: List<Pair<String, String>>) : RequestType()
}
data class SharedSecureStorageViewState(
val ready: Boolean = false,
val hasPassphrase: Boolean = true,
@ -55,13 +62,17 @@ data class SharedSecureStorageViewState(
val showResetAllAction: Boolean = false,
val userId: String = "",
val keyId: String?,
val requestedSecrets: List<String>,
val requestType: RequestType,
val resultKeyStoreAlias: String
) : MavericksState {
constructor(args: SharedSecureStorageActivity.Args) : this(
keyId = args.keyId,
requestedSecrets = args.requestedSecrets,
requestType = if (args.writeSecrets.isNotEmpty()) {
RequestType.WriteSecrets(args.writeSecrets)
} else {
RequestType.ReadSecrets(args.requestedSecrets)
},
resultKeyStoreAlias = args.resultKeyStoreAlias
)
@ -87,14 +98,17 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
setState {
copy(userId = session.myUserId)
}
val integrityResult = session.sharedSecretStorageService().checkShouldBeAbleToAccessSecrets(initialState.requestedSecrets, initialState.keyId)
if (integrityResult !is IntegrityResult.Success) {
_viewEvents.post(
SharedSecureStorageViewEvent.Error(
stringProvider.getString(R.string.enter_secret_storage_invalid),
true
)
)
if (initialState.requestType is RequestType.ReadSecrets) {
val integrityResult =
session.sharedSecretStorageService().checkShouldBeAbleToAccessSecrets(initialState.requestType.secretsName, initialState.keyId)
if (integrityResult !is IntegrityResult.Success) {
_viewEvents.post(
SharedSecureStorageViewEvent.Error(
stringProvider.getString(R.string.enter_secret_storage_invalid),
true
)
)
}
}
val keyResult = initialState.keyId?.let { session.sharedSecretStorageService().getKey(it) }
?: session.sharedSecretStorageService().getDefaultKey()
@ -226,20 +240,8 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
setState { copy(checkingSSSSAction = Fail(IllegalArgumentException(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))) }
}
withContext(Dispatchers.IO) {
initialState.requestedSecrets.forEach {
if (session.accountDataService().getUserAccountDataEvent(it) != null) {
val res = session.sharedSecretStorageService().getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec
)
decryptedSecretMap[it] = res
} else {
Timber.w("## Cannot find secret $it in SSSS, skip")
}
}
performRequest(keyInfo, keySpec, decryptedSecretMap)
}
}.fold({
setState { copy(checkingSSSSAction = Success(Unit)) }
@ -258,6 +260,37 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
}
}
private suspend fun performRequest(keyInfo: KeyInfo, keySpec: RawBytesKeySpec, decryptedSecretMap: HashMap<String, String>) {
when (val requestType = initialState.requestType) {
is RequestType.ReadSecrets -> {
requestType.secretsName.forEach {
if (session.accountDataService().getUserAccountDataEvent(it) != null) {
val res = session.sharedSecretStorageService().getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec
)
decryptedSecretMap[it] = res
} else {
Timber.w("## Cannot find secret $it in SSSS, skip")
}
}
}
is RequestType.WriteSecrets -> {
requestType.secretsNameValue.forEach {
val (name, value) = it
session.sharedSecretStorageService().storeSecret(
name = name,
secretBase64 = value,
keys = listOf(SharedSecretStorageService.KeyRef(keyInfo.id, keySpec))
)
decryptedSecretMap[name] = value
}
}
}
}
private fun handleSubmitPassphrase(action: SharedSecureStorageAction.SubmitPassphrase) {
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
val decryptedSecretMap = HashMap<String, String>()
@ -302,17 +335,8 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
)
withContext(Dispatchers.IO) {
initialState.requestedSecrets.forEach {
if (session.accountDataService().getUserAccountDataEvent(it) != null) {
val res = session.sharedSecretStorageService().getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec
)
decryptedSecretMap[it] = res
} else {
Timber.w("## Cannot find secret $it in SSSS, skip")
}
withContext(Dispatchers.IO) {
performRequest(keyInfo, keySpec, decryptedSecretMap)
}
}
}.fold({

View File

@ -95,14 +95,12 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetV
when (it) {
is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
is VerificationBottomSheetViewEvents.AccessSecretStore -> {
secretStartForActivityResult.launch(
SharedSecureStorageActivity.newIntent(
requireContext(),
null, // use default key
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME),
SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
)
)
secretStartForActivityResult.launch(SharedSecureStorageActivity.newReadIntent(
requireContext(),
null, // use default key
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME),
SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
))
}
is VerificationBottomSheetViewEvents.ModalError -> {
MaterialAlertDialogBuilder(requireContext())

View File

@ -1513,6 +1513,7 @@
<string name="keys_backup_settings_status_not_setup">Your keys are not being backed up from this session.</string>
<string name="keys_backup_settings_signature_from_unknown_device">Backup has a signature from unknown session with ID %s.</string>
<string name="keys_backup_settings_signature_from_this_user">Backup has a valid signature from this user.</string>
<string name="keys_backup_settings_valid_signature_from_this_device">Backup has a valid signature from this session.</string>
<string name="keys_backup_settings_valid_signature_from_verified_device">Backup has a valid signature from verified session %s.</string>
<string name="keys_backup_settings_valid_signature_from_unverified_device">Backup has a valid signature from unverified session %s</string>