Merge pull request #1206 from vector-im/feature/gossip_keybackup_key

Feature/gossip keybackup key
This commit is contained in:
Valere 2020-04-07 18:06:05 +02:00 committed by GitHub
commit 1d8ed387bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 186 additions and 17 deletions

View file

@ -10,6 +10,8 @@ Improvements 🙌:
- Verification DM / Handle concurrent .start after .ready (#794)
- Cross-Signing | Update Shield Logic for DM (#963)
- Cross-Signing | Complete security new session design update (#1135)
- Cross-Signing | Setup key backup as part of SSSS bootstrapping (#1201)
- Cross-Signing | Gossip key backup recovery key (#1200)
Bugfix 🐛:
- Missing avatar/displayname after verification request message (#841)

View file

@ -34,6 +34,8 @@ import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.internal.crypto.GossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
@ -197,6 +199,16 @@ class KeyShareTests : InstrumentedTest {
), it)
}
// Also bootstrap keybackup on first session
val creationInfo = mTestHelper.doSync<MegolmBackupCreationInfo> {
aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = mTestHelper.doSync<KeysVersion> {
aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
// Save it for gossiping
aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
val aliceSession2 = mTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true))
val aliceVerificationService1 = aliceSession1.cryptoService().verificationService()
@ -260,9 +272,18 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.waitWithLatch(60_000) { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
Log.d("#TEST", "CAN XS :${ aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
aliceSession2.cryptoService().crossSigningService().canCrossSign()
}
}
// Test that key backup key has been shared to
mTestHelper.waitWithLatch(60_000) { latch ->
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
mTestHelper.retryPeriodicallyWithLatch(latch) {
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey != creationInfo.recoveryKey
}
}
}
}

View file

@ -21,3 +21,5 @@ const val MASTER_KEY_SSSS_NAME = "m.cross_signing.master"
const val USER_SIGNING_KEY_SSSS_NAME = "m.cross_signing.user_signing"
const val SELF_SIGNING_KEY_SSSS_NAME = "m.cross_signing.self_signing"
const val KEYBACKUP_SECRET_SSSS_NAME = "m.megolm_backup.v1"

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCre
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo
interface KeysBackupService {
/**
@ -172,6 +173,8 @@ interface KeysBackupService {
password: String,
callback: MatrixCallback<Unit>)
fun onSecretKeyGossip(secret: String)
/**
* Restore a backup with a recovery key from a given backup version stored on the homeserver.
*
@ -210,4 +213,8 @@ interface KeysBackupService {
val isEnabled: Boolean
val isStucked: Boolean
val state: KeysBackupState
// For gossiping
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
}

View file

@ -33,6 +33,7 @@ import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
@ -239,7 +240,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask
.configureWith {
// this.executionThread = TaskThread.CRYPTO
// this.executionThread = TaskThread.CRYPTO
this.callback = callback
}
.executeBy(taskExecutor)
@ -635,7 +636,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
@Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return internalDecryptEvent(event, timeline)
return internalDecryptEvent(event, timeline)
}
/**
@ -766,19 +767,30 @@ internal class DefaultCryptoService @Inject constructor(
return
}
when (existingRequest.secretName) {
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer?
Timber.v("## onSecretSend() : secret not handled by SDK")
}
}
/**
* Returns true if handled by SDK, otherwise should be sent to application layer
*/
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
return when (secretName) {
SELF_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretSSKGossip(secretContent.secretValue)
return
crossSigningService.onSecretSSKGossip(secretValue)
true
}
USER_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretUSKGossip(secretContent.secretValue)
return
crossSigningService.onSecretUSKGossip(secretValue)
true
}
else -> {
// Ask to application layer?
Timber.v("## onSecretSend() : secret not handled by SDK")
KEYBACKUP_SECRET_SSSS_NAME -> {
keysBackupService.onSecretKeyGossip(secretValue)
true
}
else -> false
}
}

View file

@ -17,14 +17,16 @@
package im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.rest.GossipingDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
@ -281,6 +283,10 @@ internal class IncomingGossipingRequestManager @Inject constructor(
when (secretName) {
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
?.let {
extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding()
}
else -> null
}?.let { secretValue ->
Timber.i("## GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
@ -301,6 +307,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
return
}
Timber.v("## GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer")
request.ignore = Runnable {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}

View file

@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.MegolmSessionData
import im.vector.matrix.android.internal.crypto.ObjectSigner
import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
@ -67,6 +68,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyF
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId
@ -580,6 +582,31 @@ internal class DefaultKeysBackupService @Inject constructor(
}
}
override fun onSecretKeyGossip(secret: String) {
Timber.i("## CrossSigning - onSecretKeyGossip")
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
try {
val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit)
val recoveryKey = computeRecoveryKey(secret.fromBase64())
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
awaitCallback<Unit> {
trustKeysBackupVersion(keysBackupVersion, true, it)
}
val importResult = awaitCallback<ImportRoomKeysResult> {
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
}
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
} else {
Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
}
} catch (failure: Throwable) {
Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}")
}
}
}
/**
* Get public key from a Recovery key
*
@ -1391,6 +1418,14 @@ internal class DefaultKeysBackupService @Inject constructor(
.executeBy(taskExecutor)
}
override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? {
return cryptoStore.getKeyBackupRecoveryKeyInfo()
}
override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) {
cryptoStore.saveBackupRecoveryKey(recoveryKey, version)
}
companion object {
// Maximum delay in ms in {@link maybeBackupKeys}
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L

View file

@ -405,6 +405,9 @@ internal interface IMXCryptoStore {
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean)

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.crypto.store
data class SavedKeyBackupKeyInfo(
val recoveryKey : String,
val version: String
)

View file

@ -45,6 +45,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.toEntity
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper
@ -216,8 +217,8 @@ internal class RealmCryptoStore @Inject constructor(
override fun getOrCreateOlmAccount(): OlmAccount {
doRealmTransaction(realmConfiguration) {
val metaData = it.where<CryptoMetadataEntity>().findFirst()
val existing = metaData!!.getOlmAccount()
val metaData = it.where<CryptoMetadataEntity>().findFirst()
val existing = metaData!!.getOlmAccount()
if (existing == null) {
Timber.d("## Crypto Creating olm account")
val created = OlmAccount()
@ -389,6 +390,29 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
keyBackupRecoveryKey = recoveryKey
keyBackupRecoveryKeyVersion = version
}
}
}
override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? {
return doRealmQueryAndCopy(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()
}?.let {
val key = it.keyBackupRecoveryKey
val version = it.keyBackupRecoveryKeyVersion
if (!key.isNullOrBlank() && !version.isNullOrBlank()) {
SavedKeyBackupKeyInfo(recoveryKey = key, version = version)
} else {
null
}
}
}
override fun storeSSKPrivateKey(ssk: String?) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply {

View file

@ -37,13 +37,14 @@ import timber.log.Timber
internal object RealmCryptoStoreMigration : RealmMigration {
// Version 1L added Cross Signing info persistence
const val CRYPTO_STORE_SCHEMA_VERSION = 2L
const val CRYPTO_STORE_SCHEMA_VERSION = 3L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -185,4 +186,11 @@ internal object RealmCryptoStoreMigration : RealmMigration {
.addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR)
.addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
}
private fun migrateTo3(realm: DynamicRealm) {
Timber.d("Updating CryptoMetadataEntity table")
realm.schema.get("CryptoMetadataEntity")
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java)
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java)
}
}

View file

@ -38,7 +38,9 @@ internal open class CryptoMetadataEntity(
var xSignMasterPrivateKey: String? = null,
var xSignUserPrivateKey: String? = null,
var xSignSelfSignedPrivateKey: String? = null
var xSignSelfSignedPrivateKey: String? = null,
var keyBackupRecoveryKey: String? = null,
var keyBackupRecoveryKeyVersion: String? = null
// var crossSigningInfoEntity: CrossSigningInfoEntity? = null
) : RealmObject() {

View file

@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
@ -103,6 +104,7 @@ internal abstract class DefaultVerificationTransaction(
if (otherUserId == userId && !crossSigningService.canCrossSign()) {
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
}
}

View file

@ -27,6 +27,8 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.util.awaitCallback
@ -36,6 +38,7 @@ import im.vector.riotx.core.resources.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -195,7 +198,24 @@ class BootstrapCrossSigningTask @Inject constructor(
return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure)
}
// TODO configure key backup?
params.progressListener?.onProgress(
WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_key_backup),
isIndeterminate = true
)
)
try {
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = awaitCallback<KeysVersion> {
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
// Save it for gossiping
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
} catch (failure: Throwable) {
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
}
return BootstrapResult.Success(keyInfo)
}

View file

@ -71,6 +71,7 @@
<string name="bootstrap_crosssigning_progress_save_msk">Synchronizing Master key</string>
<string name="bootstrap_crosssigning_progress_save_usk">Synchronizing User key</string>
<string name="bootstrap_crosssigning_progress_save_ssk">Synchronizing Self Signing key</string>
<string name="bootstrap_crosssigning_progress_key_backup">Setting Up Key Backup</string>
<!-- %1$s is replaced by message_key and %2$s by recovery_passphrase -->