mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Prevent 4S / megolm backup desync + sign with MSK
This commit is contained in:
parent
3f8ddbec60
commit
22e0506814
1
changelog.d/5906.bugfix
Normal file
1
changelog.d/5906.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Desynchronized 4S | Megolm backup causing Unusable backup
|
@ -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()
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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),
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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())
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user