SDK / update trust on key change + live method in Service

This commit is contained in:
Valere 2020-01-27 09:25:16 +01:00
parent d60351bcb7
commit 665c577747
12 changed files with 223 additions and 42 deletions

View File

@ -16,7 +16,9 @@
package im.vector.matrix.android.api.session.crypto.crosssigning
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
import im.vector.matrix.android.internal.crypto.model.rest.SignatureUploadResponse
@ -40,6 +42,8 @@ interface CrossSigningService {
fun getUserCrossSigningKeys(userId: String): MXCrossSigningInfo?
fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>>
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
fun canCrossSign(): Boolean

View File

@ -23,13 +23,13 @@ data class MXCrossSigningInfo(
var userId: String,
var crossSigningKeys: List<CryptoCrossSigningKey> = ArrayList(),
// TODO this should at the key level no?
val isTrusted: Boolean = false
var crossSigningKeys: List<CryptoCrossSigningKey> = ArrayList()
) {
fun isTrusted() : Boolean = masterKey()?.trustLevel?.isVerified() == true
&& selfSigningKey()?.trustLevel?.isVerified() == true
fun masterKey(): CryptoCrossSigningKey? = crossSigningKeys
.firstOrNull { it.usages?.contains(KeyUsage.MASTER.value) == true }

View File

@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import okhttp3.internal.toImmutableList
import timber.log.Timber
import javax.inject.Inject
@ -38,6 +39,30 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val credentials: Credentials,
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
interface UserDevicesUpdateListener {
fun onUsersDeviceUpdate(users: List<String>)
}
private val deviceChangeListeners = ArrayList<UserDevicesUpdateListener>()
fun addListener(listener: UserDevicesUpdateListener) {
deviceChangeListeners.add(listener)
}
fun removeListener(listener: UserDevicesUpdateListener) {
deviceChangeListeners.remove(listener)
}
fun dispatchDeviceChange(users: List<String>) {
deviceChangeListeners.forEach {
try {
it.onUsersDeviceUpdate(users)
} catch (failure: Throwable) {
Timber.e(failure, "Failed to dispatch device chande")
}
}
}
// HS not ready for retry
private val notReadyToRetryHS = mutableSetOf<String>()
@ -210,6 +235,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
}
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
dispatchDeviceChange(userIds.toImmutableList())
return usersDevicesInfoMap
}
@ -338,6 +364,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
userSigningKey
)
}
// Update devices trust for these users
dispatchDeviceChange(downloadUsers)
return onKeysDownloadSucceed(filteredUsers, response.failures)
}

View File

@ -16,12 +16,14 @@
package im.vector.matrix.android.internal.crypto.crosssigning
import androidx.lifecycle.LiveData
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningState
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
@ -63,7 +65,7 @@ internal class DefaultCrossSigningService @Inject constructor(
private val uploadSignaturesTask: UploadSignaturesTask,
private val cryptoCoroutineScope: CoroutineScope,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : CrossSigningService {
private val taskExecutor: TaskExecutor) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null
@ -120,16 +122,22 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
}
// Recover local trust in case private key are there?
cryptoStore.setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified())
}
} catch (e: Throwable) {
// Mmm this kind of a big issue
Timber.e(e, "Failed to initialize Cross Signing")
}
deviceListManager.addListener(this)
}
fun release() {
olmUtility?.releaseUtility()
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
deviceListManager.removeListener(this)
}
protected fun finalize() {
@ -251,13 +259,13 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
resetTrustOnKeyChange()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
this.retryCount = 3
this.constraints = TaskConstraints(true)
this.callback = object : MatrixCallback<SignatureUploadResponse> {
override fun onSuccess(data: SignatureUploadResponse) {
Timber.i("## CrossSigning - signatures succesfuly uploaded")
// Force download of my keys now
kotlin.runCatching {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
@ -291,6 +299,12 @@ internal class DefaultCrossSigningService @Inject constructor(
}.executeBy(taskExecutor)
}
private fun resetTrustOnKeyChange() {
Timber.i("## CrossSigning - Clear all other user trust")
cryptoStore.clearOtherUserTrust()
}
/**
*
*
@ -310,7 +324,7 @@ internal class DefaultCrossSigningService @Inject constructor(
*
*/
override fun isUserTrusted(userId: String): Boolean {
return cryptoStore.getCrossSigningInfo(userId)?.isTrusted == true
return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true
}
/**
@ -331,7 +345,7 @@ internal class DefaultCrossSigningService @Inject constructor(
val myUserKey = myCrossSigningInfo?.userKey()
?: return UserTrustResult.CrossSigningNotConfigured(credentials.userId)
if (!myCrossSigningInfo.isTrusted) {
if (!myCrossSigningInfo.isTrusted()) {
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
}
@ -455,6 +469,10 @@ internal class DefaultCrossSigningService @Inject constructor(
return cryptoStore.getCrossSigningInfo(userId)
}
override fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>> {
return cryptoStore.getLiveCrossSigningInfo(userId)
}
override fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
return cryptoStore.getMyCrossSigningInfo()
}
@ -464,20 +482,21 @@ internal class DefaultCrossSigningService @Inject constructor(
}
override fun trustUser(userId: String, callback: MatrixCallback<SignatureUploadResponse>) {
Timber.d("## CrossSigning - Mark user $userId as trusted ")
// We should have this user keys
val otherMasterKeys = getUserCrossSigningKeys(userId)?.masterKey()
if (otherMasterKeys == null) {
callback.onFailure(Throwable("Other master signing key is not known"))
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
return
}
val myKeys = getUserCrossSigningKeys(credentials.userId)
if (myKeys == null) {
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
return
}
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
if (userPubKey == null || userPkSigning == null) {
callback.onFailure(Throwable("Cannot sign from this account, privateKeyUnknown $userPubKey"))
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
return
}
@ -487,18 +506,45 @@ internal class DefaultCrossSigningService @Inject constructor(
if (newSignature == null) {
// race??
callback.onFailure(Throwable("Failed to sign"))
callback.onFailure(Throwable("## CrossSigning - Failed to sign"))
return
}
cryptoStore.setUserKeysAsTrusted(userId, true)
// TODO update local copy with new signature directly here? kind of local echo of trust?
Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK")
val uploadQuery = UploadSignatureQueryBuilder()
.withSigningKeyInfo(otherMasterKeys.addSignatureAndCopy(credentials.userId, userPubKey, newSignature))
.withSigningKeyInfo(otherMasterKeys.copyForSignature(credentials.userId, userPubKey, newSignature))
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.callback = callback
this.callback = object: MatrixCallback<SignatureUploadResponse> {
override fun onSuccess(data: SignatureUploadResponse) {
//force a key download to refresh trust?
val uldata = data
kotlin.runCatching {
Timber.d("## CrossSigning - Force download of user keys")
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
deviceListManager.downloadKeys(listOf(userId), true)
}
}.foldToCallback(object : MatrixCallback<Any> {
override fun onSuccess(data: Any) {
callback.onSuccess(uldata)
}
override fun onFailure(failure: Throwable) {
Timber.e("## CrossSigning - fail to download keys", failure)
callback.onFailure(failure)
}
})
}
override fun onFailure(failure: Throwable) {
Timber.e("## CrossSigning - fail to upload signature", failure)
callback.onFailure(failure)
}
}
}.executeBy(taskExecutor)
}
@ -570,13 +616,13 @@ internal class DefaultCrossSigningService @Inject constructor(
val myKeys = getUserCrossSigningKeys(credentials.userId)
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(credentials.userId))
if (!myKeys.isTrusted) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
val otherKeys = getUserCrossSigningKeys(userId)
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
// TODO should we force verification ?
if (!otherKeys.isTrusted) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
// Check if the trust chain is valid
/*
@ -618,4 +664,24 @@ internal class DefaultCrossSigningService @Inject constructor(
crossSignTrustFail
}
}
override fun onUsersDeviceUpdate(users: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${users.size} users")
users.forEach { userId ->
checkUserTrust(userId).let {
Timber.d("## CrossSigning - update trust for ${userId} , verified=${it.isVerified()}")
cryptoStore.setUserKeysAsTrusted(userId, it.isVerified())
}
// TODO if my keys have changes, i should recheck all devices of all users?
val devices = cryptoStore.getUserDeviceList(userId)
devices?.forEach { device ->
val updatedTrust = checkDeviceTrust(userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user ${userId} , verified=$updatedTrust")
cryptoStore.setDeviceTrust(userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
}
}
}

View File

@ -29,3 +29,4 @@ sealed class DeviceTrustResult {
fun DeviceTrustResult.isSuccess(): Boolean = this is DeviceTrustResult.Success
fun DeviceTrustResult.isCrossSignedVerified(): Boolean = (this as? DeviceTrustResult.Success)?.level?.isCrossSigningVerified() == true
fun DeviceTrustResult.isLocallyVerified(): Boolean = (this as? DeviceTrustResult.Success)?.level?.isLocallyVerified() == true

View File

@ -41,6 +41,12 @@ data class CryptoCrossSigningKey(
)
}
fun copyForSignature(userId: String, signedWithNoPrefix: String, signature: String): CryptoCrossSigningKey {
return this.copy(
signatures = mapOf(userId to mapOf("ed25519:$signedWithNoPrefix" to signature))
)
}
data class Builder(
val userId: String,
val usage: KeyUsage,

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.store
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
@ -403,10 +404,14 @@ internal interface IMXCryptoStore {
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo?
fun getLiveCrossSigningInfo(userId: String) : LiveData<Optional<MXCrossSigningInfo>>
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
fun getCrossSigningPrivateKeys() : PrivateKeysInfo?
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified : Boolean)
fun clearOtherUserTrust()
}

View File

@ -21,9 +21,12 @@ import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
@ -883,17 +886,54 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) {
doRealmTransaction(realmConfiguration) { realm ->
val info = realm.where(CrossSigningInfoEntity::class.java)
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
if (info != null) {
xInfoEntity?.crossSigningKeys?.forEach { info ->
val level = info.trustLevelEntity
if (level == null) {
val newLevel = realm.createObject(TrustLevelEntity::class.java)
newLevel.locallyVerified = true
newLevel.locallyVerified = trusted
newLevel.crossSignedVerified = trusted
info.trustLevelEntity = newLevel
} else {
level.locallyVerified = true
level.locallyVerified = trusted
level.crossSignedVerified = trusted
}
}
}
}
override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where(DeviceInfoEntity::class.java)
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
.findFirst()?.let { deviceInfoEntity ->
val trustEntity = deviceInfoEntity.trustLevelEntity
if (trustEntity == null) {
realm.createObject(TrustLevelEntity::class.java).let {
it.locallyVerified = locallyVerified
it.crossSignedVerified = crossSignedVerified
deviceInfoEntity.trustLevelEntity = it
}
} else {
trustEntity.locallyVerified = locallyVerified
trustEntity.crossSignedVerified = crossSignedVerified
}
}
}
}
override fun clearOtherUserTrust() {
doRealmTransaction(realmConfiguration) { realm ->
val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java)
.findAll()
xInfoEntities?.forEach { info ->
//Need to ignore mine
if (info.userId != credentials.userId) {
info.crossSigningKeys.forEach {
it.trustLevelEntity = null
}
}
}
}
@ -913,15 +953,49 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
userId = userId,
keys = mapOf("ed25519:$pubKey" to pubKey),
usages = it.usages.map { it },
signatures = it.getSignatures()
signatures = it.getSignatures(),
trustLevel = it.trustLevelEntity?.let {
DeviceTrustLevel(
crossSigningVerified = it.crossSignedVerified ?: false,
locallyVerified = it.locallyVerified ?: false
)
}
)
},
isTrusted = xsignInfo.trustLevelEntity?.isVerified() ?: false // TODO
}
)
}
}
override fun getLiveCrossSigningInfo(userId: String): LiveData<Optional<MXCrossSigningInfo>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm -> realm.where<CrossSigningInfoEntity>().equalTo(UserEntityFields.USER_ID, userId) },
{ entity ->
MXCrossSigningInfo(
userId = userId,
crossSigningKeys = entity.crossSigningKeys.mapNotNull {
val pubKey = it.publicKeyBase64 ?: return@mapNotNull null
CryptoCrossSigningKey(
userId = userId,
keys = mapOf("ed25519:$pubKey" to pubKey),
usages = it.usages.map { it },
signatures = it.getSignatures(),
trustLevel = it.trustLevelEntity?.let {
DeviceTrustLevel(
crossSigningVerified = it.crossSignedVerified ?: false,
locallyVerified = it.locallyVerified ?: false
)
}
)
}
)
}
)
return Transformations.map(liveData) {
it.firstOrNull().toOptional()
}
}
override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) {
doRealmTransaction(realmConfiguration) { realm ->
@ -941,19 +1015,13 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
keyInfoEntity.usages = info.usages?.let { RealmList(*it.toTypedArray()) }
?: RealmList()
keyInfoEntity.putSignatures(info.signatures)
// TODO how to handle better, check if same keys?
// reset trust
keyInfoEntity.trustLevelEntity = null
}
)
}
existing.crossSigningKeys = xkeys
val existingTrust = existing.trustLevelEntity
if (existingTrust != null) {
existingTrust.locallyVerified = true
} else {
realm.createObject(TrustLevelEntity::class.java).let {
it.locallyVerified = info.isTrusted
existing.trustLevelEntity = it
}
}
}
}
}

View File

@ -43,22 +43,23 @@ internal object RealmCryptoStoreMigration : RealmMigration {
Timber.d("Step 0 -> 1")
Timber.d("Create KeyInfoEntity")
val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity")
.addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java)
.addField(KeyInfoEntityFields.SIGNATURES, String::class.java)
.addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java)
val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity")
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
.addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java)
.setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true)
val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity")
.addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java)
.addField(KeyInfoEntityFields.SIGNATURES, String::class.java)
.addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java)
.addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema)
Timber.d("Create CrossSigningInfoEntity")
val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity")
.addField(CrossSigningInfoEntityFields.USER_ID, String::class.java)
.addRealmObjectField(CrossSigningInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema)
.addPrimaryKey(CrossSigningInfoEntityFields.USER_ID)
.addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema)

View File

@ -24,8 +24,7 @@ import io.realm.annotations.PrimaryKey
internal open class CrossSigningInfoEntity(
@PrimaryKey
var userId: String? = null,
var crossSigningKeys: RealmList<KeyInfoEntity> = RealmList(),
var trustLevelEntity: TrustLevelEntity? = null
var crossSigningKeys: RealmList<KeyInfoEntity> = RealmList()
) : RealmObject() {
companion object

View File

@ -29,7 +29,8 @@ internal open class KeyInfoEntity(
* The signature of this MXDeviceInfo.
* A map from "<userId>" to a map from "<key type>:<Publickey>" to "<signature>"
*/
var signatures: String? = null
var signatures: String? = null,
var trustLevelEntity: TrustLevelEntity? = null
) : RealmObject() {
// Deserialize data

View File

@ -163,7 +163,7 @@ internal abstract class SASDefaultVerificationTransaction(
keyMap[keyId] = macString
cryptoStore.getMyCrossSigningInfo()?.takeIf { it.isTrusted }
cryptoStore.getMyCrossSigningInfo()?.takeIf { it.isTrusted() }
?.masterKey()
?.unpaddedBase64PublicKey
?.let { masterPublicKey ->