Merge pull request #1015 from vector-im/feature/new_signin_passphrase

Feature/new signin passphrase
This commit is contained in:
Benoit Marty 2020-02-26 13:56:31 +01:00 committed by GitHub
commit b7cf7e06a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1368 additions and 285 deletions

View File

@ -1,6 +1,9 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="160" /> <option name="RIGHT_MARGIN" value="160" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS"> <option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value> <value>

View File

@ -138,7 +138,7 @@ class XSigningTest : InstrumentedTest {
// Manually mark it as trusted from first session // Manually mark it as trusted from first session
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService().signDevice(bobSecondDeviceId, it) bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it)
} }
// Now alice should cross trust bob's second device // Now alice should cross trust bob's second device

View File

@ -16,26 +16,25 @@
package im.vector.matrix.android.internal.crypto.ssss package im.vector.matrix.android.internal.crypto.ssss
import android.util.Base64
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent
import im.vector.matrix.android.api.session.securestorage.KeySigner import im.vector.matrix.android.api.session.securestorage.KeySigner
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.SessionTestParams import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.common.TestMatrixCallback import im.vector.matrix.android.common.TestMatrixCallback
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2 import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService
import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -71,7 +70,7 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key" val TEST_KEY_ID = "my.test.Key"
val ssssKeyCreationInfo = mTestHelper.doSync<SsssKeyCreationInfo> { mTestHelper.doSync<SsssKeyCreationInfo> {
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it) quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it)
} }
@ -95,16 +94,9 @@ class QuadSTests : InstrumentedTest {
assertNotNull("Key should be stored in account data", accountData) assertNotNull("Key should be stored in account data", accountData)
val parsed = SecretStorageKeyContent.fromJson(accountData!!.content) val parsed = SecretStorageKeyContent.fromJson(accountData!!.content)
assertNotNull("Key Content cannot be parsed", parsed) assertNotNull("Key Content cannot be parsed", parsed)
assertEquals("Unexpected Algorithm", SSSS_ALGORITHM_CURVE25519_AES_SHA2, parsed!!.algorithm) assertEquals("Unexpected Algorithm", SSSS_ALGORITHM_AES_HMAC_SHA2, parsed!!.algorithm)
assertEquals("Unexpected key name", "Test Key", parsed.name) assertEquals("Unexpected key name", "Test Key", parsed.name)
assertNull("Key was not generated from passphrase", parsed.passphrase) assertNull("Key was not generated from passphrase", parsed.passphrase)
assertNotNull("Pubkey should be defined", parsed.publicKey)
val privateKeySpec = Curve25519AesSha2KeySpec.fromRecoveryKey(ssssKeyCreationInfo.recoveryKey)
val pubKey = withOlmDecryption { olmPkDecryption ->
olmPkDecryption.setPrivateKey(privateKeySpec!!.privateKey)
}
assertEquals("Unexpected Public Key", pubKey, parsed.publicKey)
// Set as default key // Set as default key
quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback<Unit> {}) quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback<Unit> {})
@ -137,13 +129,15 @@ class QuadSTests : InstrumentedTest {
val keyId = "My.Key" val keyId = "My.Key"
val info = generatedSecret(aliceSession, keyId, true) val info = generatedSecret(aliceSession, keyId, true)
val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey)
// Store a secret // Store a secret
val clearSecret = Base64.encodeToString("42".toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP) val clearSecret = "42".toByteArray().toBase64NoPadding()
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
aliceSession.sharedSecretStorageService.storeSecret( aliceSession.sharedSecretStorageService.storeSecret(
"secret.of.life", "secret.of.life",
clearSecret, clearSecret,
null, // default key listOf(SharedSecretStorageService.KeyRef(null, keySpec)), // default key
it it
) )
} }
@ -157,14 +151,13 @@ class QuadSTests : InstrumentedTest {
val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId)) val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId))
assertNotNull(secret?.ciphertext) assertNotNull(secret?.ciphertext)
assertNotNull(secret?.mac) assertNotNull(secret?.mac)
assertNotNull(secret?.ephemeral) assertNotNull(secret?.initializationVector)
// Try to decrypt?? // Try to decrypt??
val keySpec = Curve25519AesSha2KeySpec.fromRecoveryKey(info.recoveryKey)
val decryptedSecret = mTestHelper.doSync<String> { val decryptedSecret = mTestHelper.doSync<String> {
aliceSession.sharedSecretStorageService.getSecret("secret.of.life", aliceSession.sharedSecretStorageService.getSecret(
"secret.of.life",
null, // default key null, // default key
keySpec!!, keySpec!!,
it it
@ -209,7 +202,10 @@ class QuadSTests : InstrumentedTest {
aliceSession.sharedSecretStorageService.storeSecret( aliceSession.sharedSecretStorageService.storeSecret(
"my.secret", "my.secret",
mySecretText.toByteArray().toBase64NoPadding(), mySecretText.toByteArray().toBase64NoPadding(),
listOf(keyId1, keyId2), listOf(
SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)),
SharedSecretStorageService.KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey))
),
it it
) )
} }
@ -226,7 +222,7 @@ class QuadSTests : InstrumentedTest {
mTestHelper.doSync<String> { mTestHelper.doSync<String> {
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1, keyId1,
Curve25519AesSha2KeySpec.fromRecoveryKey(key1Info.recoveryKey)!!, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!,
it it
) )
} }
@ -234,7 +230,7 @@ class QuadSTests : InstrumentedTest {
mTestHelper.doSync<String> { mTestHelper.doSync<String> {
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId2, keyId2,
Curve25519AesSha2KeySpec.fromRecoveryKey(key2Info.recoveryKey)!!, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!,
it it
) )
} }
@ -255,7 +251,7 @@ class QuadSTests : InstrumentedTest {
aliceSession.sharedSecretStorageService.storeSecret( aliceSession.sharedSecretStorageService.storeSecret(
"my.secret", "my.secret",
mySecretText.toByteArray().toBase64NoPadding(), mySecretText.toByteArray().toBase64NoPadding(),
listOf(keyId1), listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))),
it it
) )
} }
@ -264,7 +260,7 @@ class QuadSTests : InstrumentedTest {
var error = false var error = false
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1, keyId1,
Curve25519AesSha2KeySpec.fromPassphrase( RawBytesKeySpec.fromPassphrase(
"A bad passphrase", "A bad passphrase",
key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.salt ?: "",
key1Info.content?.passphrase?.iterations ?: 0, key1Info.content?.passphrase?.iterations ?: 0,
@ -289,7 +285,7 @@ class QuadSTests : InstrumentedTest {
mTestHelper.doSync<String> { mTestHelper.doSync<String> {
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1, keyId1,
Curve25519AesSha2KeySpec.fromPassphrase( RawBytesKeySpec.fromPassphrase(
passphrase, passphrase,
key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.salt ?: "",
key1Info.content?.passphrase?.iterations ?: 0, key1Info.content?.passphrase?.iterations ?: 0,

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.api.extensions package im.vector.matrix.android.api.extensions
import im.vector.matrix.android.api.comparators.DatedObjectComparators
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@ -33,7 +32,5 @@ fun CryptoDeviceInfo.getFingerprintHumanReadable() = fingerprint()
* ========================================================================================== */ * ========================================================================================== */
fun List<DeviceInfo>.sortByLastSeen(): List<DeviceInfo> { fun List<DeviceInfo>.sortByLastSeen(): List<DeviceInfo> {
val list = toMutableList() return this.sortedByDescending { it.lastSeenTs ?: 0 }
list.sortWith(DatedObjectComparators.descComparator)
return list
} }

View File

@ -42,6 +42,10 @@ interface CrossSigningService {
fun initializeCrossSigning(authParams: UserPasswordAuth?, fun initializeCrossSigning(authParams: UserPasswordAuth?,
callback: MatrixCallback<Unit>? = null) callback: MatrixCallback<Unit>? = null)
fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?,
sskPrivateKey: String?) : UserTrustResult
fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo?
fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>> fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>>
@ -53,10 +57,12 @@ interface CrossSigningService {
fun trustUser(otherUserId: String, fun trustUser(otherUserId: String,
callback: MatrixCallback<Unit>) callback: MatrixCallback<Unit>)
fun markMyMasterKeyAsTrusted()
/** /**
* Sign one of your devices and upload the signature * Sign one of your devices and upload the signature
*/ */
fun signDevice(deviceId: String, fun trustDevice(deviceId: String,
callback: MatrixCallback<Unit>) callback: MatrixCallback<Unit>)
fun checkDeviceTrust(otherUserId: String, fun checkDeviceTrust(otherUserId: String,

View File

@ -0,0 +1,23 @@
/*
* 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.api.session.crypto.crosssigning
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"

View File

@ -32,7 +32,8 @@ data class EncryptedSecretContent(
/** unpadded base64-encoded ciphertext */ /** unpadded base64-encoded ciphertext */
@Json(name = "ciphertext") val ciphertext: String? = null, @Json(name = "ciphertext") val ciphertext: String? = null,
@Json(name = "mac") val mac: String? = null, @Json(name = "mac") val mac: String? = null,
@Json(name = "ephemeral") val ephemeral: String? = null @Json(name = "ephemeral") val ephemeral: String? = null,
@Json(name = "iv") val initializationVector: String? = null
) : AccountDataContent { ) : AccountDataContent {
companion object { companion object {
/** /**

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.api.session.securestorage
sealed class IntegrityResult {
data class Success(val passphraseBased: Boolean) : IntegrityResult()
data class Error(val cause: SharedSecretStorageError) : IntegrityResult()
}

View File

@ -27,5 +27,6 @@ sealed class SharedSecretStorageError(message: String?) : Throwable(message) {
object BadKeyFormat : SharedSecretStorageError("Bad Key Format") object BadKeyFormat : SharedSecretStorageError("Bad Key Format")
object ParsingError : SharedSecretStorageError("parsing Error") object ParsingError : SharedSecretStorageError("parsing Error")
object BadMac : SharedSecretStorageError("Bad mac")
data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage) data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage)
} }

View File

@ -42,7 +42,7 @@ interface SharedSecretStorageService {
*/ */
fun generateKey(keyId: String, fun generateKey(keyId: String,
keyName: String, keyName: String,
keySigner: KeySigner, keySigner: KeySigner?,
callback: MatrixCallback<SsssKeyCreationInfo>) callback: MatrixCallback<SsssKeyCreationInfo>)
/** /**
@ -92,7 +92,7 @@ interface SharedSecretStorageService {
* @param secret The secret contents. * @param secret The secret contents.
* @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret. * @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret.
*/ */
fun storeSecret(name: String, secretBase64: String, keys: List<String>?, callback: MatrixCallback<Unit>) fun storeSecret(name: String, secretBase64: String, keys: List<KeyRef>, callback: MatrixCallback<Unit>)
/** /**
* Use this call to determine which SSSSKeySpec to use for requesting secret * Use this call to determine which SSSSKeySpec to use for requesting secret
@ -104,9 +104,15 @@ interface SharedSecretStorageService {
* *
* @param name The name of the secret * @param name The name of the secret
* @param keyId The id of the key that should be used to decrypt (null for default key) * @param keyId The id of the key that should be used to decrypt (null for default key)
* @param secretKey the secret key to use (@see #Curve25519AesSha2KeySpec) * @param secretKey the secret key to use (@see #RawBytesKeySpec)
* *
*/ */
@Throws
fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>) fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>)
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult
data class KeyRef(
val keyId: String?,
val keySpec: SsssKeySpec?
)
} }

View File

@ -23,14 +23,14 @@ import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyF
/** Tag class */ /** Tag class */
interface SsssKeySpec interface SsssKeySpec
data class Curve25519AesSha2KeySpec( data class RawBytesKeySpec(
val privateKey: ByteArray val privateKey: ByteArray
) : SsssKeySpec { ) : SsssKeySpec {
companion object { companion object {
fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): Curve25519AesSha2KeySpec { fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): RawBytesKeySpec {
return Curve25519AesSha2KeySpec( return RawBytesKeySpec(
privateKey = deriveKey( privateKey = deriveKey(
passphrase, passphrase,
salt, salt,
@ -40,9 +40,9 @@ data class Curve25519AesSha2KeySpec(
) )
} }
fun fromRecoveryKey(recoveryKey: String): Curve25519AesSha2KeySpec? { fun fromRecoveryKey(recoveryKey: String): RawBytesKeySpec? {
return extractCurveKeyFromRecoveryKey(recoveryKey)?.let { return extractCurveKeyFromRecoveryKey(recoveryKey)?.let {
Curve25519AesSha2KeySpec( RawBytesKeySpec(
privateKey = it privateKey = it
) )
} }
@ -53,7 +53,7 @@ data class Curve25519AesSha2KeySpec(
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
other as Curve25519AesSha2KeySpec other as RawBytesKeySpec
if (!privateKey.contentEquals(other.privateKey)) return false if (!privateKey.contentEquals(other.privateKey)) return false

View File

@ -35,6 +35,8 @@ const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-
* Secured Shared Storage algorithm constant * Secured Shared Storage algorithm constant
*/ */
const val SSSS_ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2" const val SSSS_ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2"
/* Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. **/
const val SSSS_ALGORITHM_AES_HMAC_SHA2 = "m.secret_storage.v1.aes-hmac-sha2"
// TODO Refacto: use this constants everywhere // TODO Refacto: use this constants everywhere
const val ed25519 = "ed25519" const val ed25519 = "ed25519"

View File

@ -88,7 +88,8 @@ internal class DefaultCrossSigningService @Inject constructor(
Timber.i("## CrossSigning - Loading master key success") Timber.i("## CrossSigning - Loading master key success")
} else { } else {
Timber.w("## CrossSigning - Public master key does not match the private key") Timber.w("## CrossSigning - Public master key does not match the private key")
// TODO untrust pkSigning.releaseSigning()
// TODO untrust?
} }
} }
privateKeysInfo.user privateKeysInfo.user
@ -100,7 +101,8 @@ internal class DefaultCrossSigningService @Inject constructor(
Timber.i("## CrossSigning - Loading User Signing key success") Timber.i("## CrossSigning - Loading User Signing key success")
} else { } else {
Timber.w("## CrossSigning - Public User key does not match the private key") Timber.w("## CrossSigning - Public User key does not match the private key")
// TODO untrust pkSigning.releaseSigning()
// TODO untrust?
} }
} }
privateKeysInfo.selfSigned privateKeysInfo.selfSigned
@ -112,7 +114,8 @@ internal class DefaultCrossSigningService @Inject constructor(
Timber.i("## CrossSigning - Loading Self Signing key success") Timber.i("## CrossSigning - Loading Self Signing key success")
} else { } else {
Timber.w("## CrossSigning - Public Self Signing key does not match the private key") Timber.w("## CrossSigning - Public Self Signing key does not match the private key")
// TODO untrust pkSigning.releaseSigning()
// TODO untrust?
} }
} }
} }
@ -224,7 +227,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val myDevice = myDeviceInfoHolder.get().myDevice val myDevice = myDeviceInfoHolder.get().myDevice
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary())
val signedDevice = selfSigningPkOlm.sign(canonicalJson) val signedDevice = selfSigningPkOlm.sign(canonicalJson)
val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()).also { val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap())
.also {
it[userId] = (it[userId] it[userId] = (it[userId]
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
} }
@ -233,7 +237,8 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
// sign MSK with device key (migration) and upload signatures // sign MSK with device key (migration) and upload signatures
olmDevice.signMessage(JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()))?.let { sign -> val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary())
olmDevice.signMessage(message)?.let { sign ->
val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap() val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap()
?: HashMap()).also { ?: HashMap()).also {
it[userId] = (it[userId] it[userId] = (it[userId]
@ -292,6 +297,80 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.clearOtherUserTrust() cryptoStore.clearOtherUserTrust()
} }
override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?,
sskPrivateKey: String?
): UserTrustResult {
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId)
var masterKeyIsTrusted = false
var userKeyIsTrusted = false
var selfSignedKeyIsTrusted = false
masterKeyPrivateKey?.fromBase64NoPadding()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning?.releaseSigning()
masterPkSigning = pkSigning
masterKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
uskKeyPrivateKey?.fromBase64NoPadding()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning?.releaseSigning()
userPkSigning = pkSigning
userKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
sskPrivateKey?.fromBase64NoPadding()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning?.releaseSigning()
selfSigningPkSigning = pkSigning
selfSignedKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) {
return UserTrustResult.KeysNotTrusted(mxCrossSigningInfo)
} else {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
val checkSelfTrust = checkSelfTrust()
if (checkSelfTrust.isVerified()) {
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey)
setUserKeysAsTrusted(userId, true)
}
return checkSelfTrust
}
}
/** /**
* *
* *
@ -374,7 +453,9 @@ internal class DefaultCrossSigningService @Inject constructor(
?.fromBase64NoPadding() ?.fromBase64NoPadding()
var isMaterKeyTrusted = false var isMaterKeyTrusted = false
if (masterPrivateKey != null) { if (myMasterKey.trustLevel?.locallyVerified == true) {
isMaterKeyTrusted = true
} else if (masterPrivateKey != null) {
// Check if private match public // Check if private match public
var olmPkSigning: OlmPkSigning? = null var olmPkSigning: OlmPkSigning? = null
try { try {
@ -507,7 +588,12 @@ internal class DefaultCrossSigningService @Inject constructor(
}.executeBy(taskExecutor) }.executeBy(taskExecutor)
} }
override fun signDevice(deviceId: String, callback: MatrixCallback<Unit>) { override fun markMyMasterKeyAsTrusted() {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
checkSelfTrust()
}
override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
// This device should be yours // This device should be yours
val device = cryptoStore.getUserDevice(userId, deviceId) val device = cryptoStore.getUserDevice(userId, deviceId)
if (device == null) { if (device == null) {

View File

@ -18,17 +18,19 @@ package im.vector.matrix.android.internal.crypto.crosssigning
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject import javax.inject.Inject
@ -36,7 +38,7 @@ internal class ShieldTrustUpdater @Inject constructor(
private val eventBus: EventBus, private val eventBus: EventBus,
private val computeTrustTask: ComputeTrustTask, private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
@CryptoDatabase private val cryptoRealmConfiguration: RealmConfiguration, private val coroutineDispatchers: MatrixCoroutineDispatchers,
@SessionDatabase private val sessionRealmConfiguration: RealmConfiguration, @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
private val roomSummaryUpdater: RoomSummaryUpdater private val roomSummaryUpdater: RoomSummaryUpdater
) { ) {
@ -45,29 +47,14 @@ internal class ShieldTrustUpdater @Inject constructor(
private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD") private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
} }
private val backgroundCryptoRealm = AtomicReference<Realm>()
private val backgroundSessionRealm = AtomicReference<Realm>() private val backgroundSessionRealm = AtomicReference<Realm>()
private val isStarted = AtomicBoolean() private val isStarted = AtomicBoolean()
// private var cryptoDevicesResult: RealmResults<DeviceInfoEntity>? = null
// private val cryptoDeviceChangeListener = object : OrderedRealmCollectionChangeListener<RealmResults<DeviceInfoEntity>> {
// override fun onChange(t: RealmResults<DeviceInfoEntity>, changeSet: OrderedCollectionChangeSet) {
// val grouped = t.groupBy { it.userId }
// onCryptoDevicesChange(grouped.keys.mapNotNull { it })
// }
// }
fun start() { fun start() {
if (isStarted.compareAndSet(false, true)) { if (isStarted.compareAndSet(false, true)) {
eventBus.register(this) eventBus.register(this)
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
val cryptoRealm = Realm.getInstance(cryptoRealmConfiguration)
backgroundCryptoRealm.set(cryptoRealm)
// cryptoDevicesResult = cryptoRealm.where<DeviceInfoEntity>().findAll()
// cryptoDevicesResult?.addChangeListener(cryptoDeviceChangeListener)
backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration)) backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration))
} }
} }
@ -77,10 +64,6 @@ internal class ShieldTrustUpdater @Inject constructor(
if (isStarted.compareAndSet(true, false)) { if (isStarted.compareAndSet(true, false)) {
eventBus.unregister(this) eventBus.unregister(this)
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
// cryptoDevicesResult?.removeAllChangeListeners()
backgroundCryptoRealm.getAndSet(null).also {
it?.close()
}
backgroundSessionRealm.getAndSet(null).also { backgroundSessionRealm.getAndSet(null).also {
it?.close() it?.close()
} }
@ -93,8 +76,7 @@ internal class ShieldTrustUpdater @Inject constructor(
if (!isStarted.get()) { if (!isStarted.get()) {
return return
} }
taskExecutor.executorScope.launch(coroutineDispatchers.crypto) {
taskExecutor.executorScope.launch {
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds)) val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds))
// We need to send that back to session base // We need to send that back to session base
@ -117,31 +99,40 @@ internal class ShieldTrustUpdater @Inject constructor(
private fun onCryptoDevicesChange(users: List<String>) { private fun onCryptoDevicesChange(users: List<String>) {
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
val impactedRoomsId = backgroundSessionRealm.get().where(RoomMemberSummaryEntity::class.java) val impactedRoomsId = backgroundSessionRealm.get()?.where(RoomMemberSummaryEntity::class.java)
.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) ?.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
.findAll() ?.findAll()
.map { it.roomId } ?.map { it.roomId }
.distinct() ?.distinct()
val map = HashMap<String, List<String>>() val map = HashMap<String, List<String>>()
impactedRoomsId.forEach { roomId -> impactedRoomsId?.forEach { roomId ->
RoomMemberSummaryEntity.where(backgroundSessionRealm.get(), roomId) backgroundSessionRealm.get()?.let { realm ->
RoomMemberSummaryEntity.where(realm, roomId)
.findAll() .findAll()
.let { results -> .let { results ->
map[roomId] = results.map { it.userId } map[roomId] = results.map { it.userId }
} }
} }
}
map.forEach { entry -> map.forEach { entry ->
val roomId = entry.key val roomId = entry.key
val userList = entry.value val userList = entry.value
taskExecutor.executorScope.launch { taskExecutor.executorScope.launch {
withContext(coroutineDispatchers.crypto) {
try {
// Can throw if the crypto database has been closed in between, in this case log and ignore?
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList)) val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList))
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
backgroundSessionRealm.get()?.executeTransaction { realm -> backgroundSessionRealm.get()?.executeTransaction { realm ->
roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust) roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust)
} }
} }
} catch (failure: Throwable) {
Timber.e(failure)
}
}
} }
} }
} }

View File

@ -28,10 +28,6 @@ data class CryptoDeviceInfo(
override val keys: Map<String, String>? = null, override val keys: Map<String, String>? = null,
override val signatures: Map<String, Map<String, String>>? = null, override val signatures: Map<String, Map<String, String>>? = null,
val unsigned: JsonDict? = null, val unsigned: JsonDict? = null,
// TODO how to store if this device is verified by a user SSK, or is legacy trusted?
// I need to know if it is trusted via cross signing (Trusted because bob verified it)
var trustLevel: DeviceTrustLevel? = null, var trustLevel: DeviceTrustLevel? = null,
var isBlocked: Boolean = false var isBlocked: Boolean = false
) : CryptoInfo { ) : CryptoInfo {
@ -75,19 +71,6 @@ data class CryptoDeviceInfo(
keys?.let { map["keys"] = it } keys?.let { map["keys"] = it }
return map return map
} }
//
// /**
// * @return a dictionary of the parameters
// */
// fun toDeviceKeys(): DeviceKeys {
// return DeviceKeys(
// userId = userId,
// deviceId = deviceId,
// algorithms = algorithms!!,
// keys = keys!!,
// signatures = signatures!!
// )
// }
} }
internal fun CryptoDeviceInfo.toRest(): RestDeviceInfo { internal fun CryptoDeviceInfo.toRest(): RestDeviceInfo {

View File

@ -17,37 +17,42 @@
package im.vector.matrix.android.internal.crypto.secrets package im.vector.matrix.android.internal.crypto.secrets
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.accountdata.AccountDataService import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent
import im.vector.matrix.android.api.session.securestorage.IntegrityResult
import im.vector.matrix.android.api.session.securestorage.KeyInfo import im.vector.matrix.android.api.session.securestorage.KeyInfo
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
import im.vector.matrix.android.api.session.securestorage.KeySigner import im.vector.matrix.android.api.session.securestorage.KeySigner
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageError import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageError
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2 import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWithPassword import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWithPassword
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
import im.vector.matrix.android.internal.crypto.tools.HkdfSha256
import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption
import im.vector.matrix.android.internal.crypto.tools.withOlmEncryption
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.olm.OlmPkMessage import org.matrix.olm.OlmPkMessage
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.inject.Inject import javax.inject.Inject
import kotlin.experimental.and
private data class Key(
val publicKey: String,
@Suppress("ArrayInDataClass")
val privateKey: ByteArray
)
internal class DefaultSharedSecretStorageService @Inject constructor( internal class DefaultSharedSecretStorageService @Inject constructor(
private val accountDataService: AccountDataService, private val accountDataService: AccountDataService,
@ -57,14 +62,12 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
override fun generateKey(keyId: String, override fun generateKey(keyId: String,
keyName: String, keyName: String,
keySigner: KeySigner, keySigner: KeySigner?,
callback: MatrixCallback<SsssKeyCreationInfo>) { callback: MatrixCallback<SsssKeyCreationInfo>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val key = try { val key = try {
withOlmDecryption { olmPkDecryption -> ByteArray(32).also {
val pubKey = olmPkDecryption.generateKey() SecureRandom().nextBytes(it)
val privateKey = olmPkDecryption.privateKey()
Key(pubKey, privateKey)
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
callback.onFailure(failure) callback.onFailure(failure)
@ -73,12 +76,11 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val storageKeyContent = SecretStorageKeyContent( val storageKeyContent = SecretStorageKeyContent(
name = keyName, name = keyName,
algorithm = SSSS_ALGORITHM_CURVE25519_AES_SHA2, algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2,
passphrase = null, passphrase = null
publicKey = key.publicKey
) )
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let { val signedContent = keySigner?.sign(storageKeyContent.canonicalSignable())?.let {
storageKeyContent.copy( storageKeyContent.copy(
signatures = it signatures = it
) )
@ -96,7 +98,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
callback.onSuccess(SsssKeyCreationInfo( callback.onSuccess(SsssKeyCreationInfo(
keyId = keyId, keyId = keyId,
content = storageKeyContent, content = storageKeyContent,
recoveryKey = computeRecoveryKey(key.privateKey) recoveryKey = computeRecoveryKey(key)
)) ))
} }
} }
@ -113,19 +115,9 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener) val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener)
val pubKey = try {
withOlmDecryption { olmPkDecryption ->
olmPkDecryption.setPrivateKey(privatePart.privateKey)
}
} catch (failure: Throwable) {
callback.onFailure(failure)
return@launch
}
val storageKeyContent = SecretStorageKeyContent( val storageKeyContent = SecretStorageKeyContent(
algorithm = SSSS_ALGORITHM_CURVE25519_AES_SHA2, algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2,
passphrase = SsssPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt), passphrase = SsssPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt)
publicKey = pubKey
) )
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let { val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let {
@ -188,51 +180,19 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
return getKey(keyId) return getKey(keyId)
} }
override fun storeSecret(name: String, secretBase64: String, keys: List<String>?, callback: MatrixCallback<Unit>) { override fun storeSecret(name: String, secretBase64: String, keys: List<SharedSecretStorageService.KeyRef>, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val encryptedContents = HashMap<String, EncryptedSecretContent>() val encryptedContents = HashMap<String, EncryptedSecretContent>()
try { try {
if (keys.isNullOrEmpty()) {
// use default key
when (val key = getDefaultKey()) {
is KeyInfoResult.Success -> {
if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
val encryptedResult = withOlmEncryption { olmEncrypt ->
olmEncrypt.setRecipientKey(key.keyInfo.content.publicKey)
olmEncrypt.encrypt(secretBase64)
}
encryptedContents[key.keyInfo.id] = EncryptedSecretContent(
ciphertext = encryptedResult.mCipherText,
ephemeral = encryptedResult.mEphemeralKey,
mac = encryptedResult.mMac
)
} else {
// Unknown algorithm
callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: ""))
return@launch
}
}
is KeyInfoResult.Error -> {
callback.onFailure(key.error)
return@launch
}
}
} else {
keys.forEach { keys.forEach {
val keyId = it val keyId = it.keyId
// encrypt the content // encrypt the content
when (val key = getKey(keyId)) { when (val key = keyId?.let { getKey(keyId) } ?: getDefaultKey()) {
is KeyInfoResult.Success -> { is KeyInfoResult.Success -> {
if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_CURVE25519_AES_SHA2) { if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_AES_HMAC_SHA2) {
val encryptedResult = withOlmEncryption { olmEncrypt -> encryptAesHmacSha2(it.keySpec!!, name, secretBase64).let {
olmEncrypt.setRecipientKey(key.keyInfo.content.publicKey) encryptedContents[key.keyInfo.id] = it
olmEncrypt.encrypt(secretBase64)
} }
encryptedContents[keyId] = EncryptedSecretContent(
ciphertext = encryptedResult.mCipherText,
ephemeral = encryptedResult.mEphemeralKey,
mac = encryptedResult.mMac
)
} else { } else {
// Unknown algorithm // Unknown algorithm
callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: "")) callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: ""))
@ -245,7 +205,6 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
} }
} }
} }
}
accountDataService.updateAccountData( accountDataService.updateAccountData(
type = name, type = name,
@ -258,8 +217,109 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
callback.onFailure(failure) callback.onFailure(failure)
} }
} }
}
// Add default key /**
* Encryption algorithm m.secret_storage.v1.aes-hmac-sha2
* Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. The data is encrypted and MACed as follows:
*
* Given the secret storage key, generate 64 bytes by performing an HKDF with SHA-256 as the hash, a salt of 32 bytes
* of 0, and with the secret name as the info.
*
* The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
*
* Generate 16 random bytes, set bit 63 to 0 (in order to work around differences in AES-CTR implementations), and use
* this as the AES initialization vector.
* This becomes the iv property, encoded using base64.
*
* Encrypt the data using AES-CTR-256 using the AES key generated above.
*
* This encrypted data, encoded using base64, becomes the ciphertext property.
*
* Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above.
* The resulting MAC is base64-encoded and becomes the mac property.
* (We use AES-CTR to match file encryption and key exports.)
*/
@Throws
private fun encryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, clearDataBase64: String): EncryptedSecretContent {
secretKey as RawBytesKeySpec
val pseudoRandomKey = HkdfSha256.deriveSecret(
secretKey.privateKey,
ByteArray(32) { 0.toByte() },
secretName.toByteArray(),
64)
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
val macKey = pseudoRandomKey.copyOfRange(32, 64)
val secureRandom = SecureRandom()
val iv = ByteArray(16)
secureRandom.nextBytes(iv)
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of salt is a price we have to pay.
iv[9] = iv[9] and 0x7f
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
val secretKeySpec = SecretKeySpec(aesKey, "AES")
val ivParameterSpec = IvParameterSpec(iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
// secret are not that big, just do Final
val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64NoPadding())
require(cipherBytes.isNotEmpty())
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256")
mac.init(macKeySpec)
val digest = mac.doFinal(cipherBytes)
return EncryptedSecretContent(
ciphertext = cipherBytes.toBase64NoPadding(),
initializationVector = iv.toBase64NoPadding(),
mac = digest.toBase64NoPadding()
)
}
private fun decryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, cipherContent: EncryptedSecretContent): String {
secretKey as RawBytesKeySpec
val pseudoRandomKey = HkdfSha256.deriveSecret(
secretKey.privateKey,
ByteArray(32) { 0.toByte() },
secretName.toByteArray(),
64)
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
val macKey = pseudoRandomKey.copyOfRange(32, 64)
val iv = cipherContent.initializationVector?.fromBase64NoPadding() ?: ByteArray(16)
val cipherRawBytes = cipherContent.ciphertext!!.fromBase64NoPadding()
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
val secretKeySpec = SecretKeySpec(aesKey, "AES")
val ivParameterSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
// secret are not that big, just do Final
val decryptedSecret = cipher.doFinal(cipherRawBytes)
require(decryptedSecret.isNotEmpty())
// Check Signature
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
val digest = mac.doFinal(cipherRawBytes)
if (!cipherContent.mac?.fromBase64NoPadding()?.contentEquals(digest).orFalse()) {
throw SharedSecretStorageError.BadMac
} else {
// we are good
return decryptedSecret.toBase64NoPadding()
}
} }
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> { override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
@ -299,7 +359,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val algorithm = key.keyInfo.content val algorithm = key.keyInfo.content
if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) { if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) {
val keySpec = secretKey as? Curve25519AesSha2KeySpec ?: return Unit.also { val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also {
callback.onFailure(SharedSecretStorageError.BadKeyFormat) callback.onFailure(SharedSecretStorageError.BadKeyFormat)
} }
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
@ -317,6 +377,15 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
} }
}.foldToCallback(callback) }.foldToCallback(callback)
} }
} else if (SSSS_ALGORITHM_AES_HMAC_SHA2 == algorithm.algorithm) {
val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also {
callback.onFailure(SharedSecretStorageError.BadKeyFormat)
}
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
kotlin.runCatching {
decryptAesHmacSha2(keySpec, name, secretContent)
}.foldToCallback(callback)
}
} else { } else {
callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: "")) callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: ""))
} }
@ -327,4 +396,37 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
const val ENCRYPTED = "encrypted" const val ENCRYPTED = "encrypted"
const val DEFAULT_KEY_ID = "m.secret_storage.default_key" const val DEFAULT_KEY_ID = "m.secret_storage.default_key"
} }
override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult {
if (secretNames.isEmpty()) {
return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret("none"))
}
val keyInfoResult = if (keyId == null) {
getDefaultKey()
} else {
getKey(keyId)
}
val keyInfo = (keyInfoResult as? KeyInfoResult.Success)?.keyInfo
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: ""))
if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2
|| keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
// Unsupported algorithm
return IntegrityResult.Error(
SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "")
)
}
secretNames.forEach { secretName ->
val secretEvent = accountDataService.getAccountDataEvent(secretName)
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret(secretName))
if ((secretEvent.content["encrypted"] as? Map<*, *>)?.get(keyInfo.id) == null) {
return IntegrityResult.Error(SharedSecretStorageError.SecretNotEncryptedWithKey(secretName, keyInfo.id))
}
}
return IntegrityResult.Success(keyInfo.content.passphrase != null)
}
} }

View File

@ -413,6 +413,8 @@ internal interface IMXCryptoStore {
fun getLiveCrossSigningInfo(userId: String) : LiveData<Optional<MXCrossSigningInfo>> fun getLiveCrossSigningInfo(userId: String) : LiveData<Optional<MXCrossSigningInfo>>
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean)
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
fun getCrossSigningPrivateKeys() : PrivateKeysInfo? fun getCrossSigningPrivateKeys() : PrivateKeysInfo?

View File

@ -1094,6 +1094,23 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.userId?.let { myUserId ->
CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity ->
val level = xInfoEntity.trustLevelEntity
if (level == null) {
val newLevel = realm.createObject(TrustLevelEntity::class.java)
newLevel.locallyVerified = trusted
xInfoEntity.trustLevelEntity = newLevel
} else {
level.locallyVerified = trusted
}
}
}
}
}
private fun addOrUpdateCrossSigningInfo(realm: Realm, userId: String, info: MXCrossSigningInfo?): CrossSigningInfoEntity? { private fun addOrUpdateCrossSigningInfo(realm: Realm, userId: String, info: MXCrossSigningInfo?): CrossSigningInfoEntity? {
var existing = CrossSigningInfoEntity.get(realm, userId) var existing = CrossSigningInfoEntity.get(realm, userId)
if (info == null) { if (info == null) {

View File

@ -83,11 +83,14 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
} }
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
// Relates to is not encrypted
val relatesToEventId = event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
if (event.senderId == userId) { if (event.senderId == userId) {
// If it's send from me, we need to keep track of Requests or Start // If it's send from me, we need to keep track of Requests or Start
// done from another device of mine // done from another device of mine
if (EventType.MESSAGE == event.type) { if (EventType.MESSAGE == event.getClearType()) {
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let { event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
@ -98,26 +101,26 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
} }
} }
} }
} else if (EventType.KEY_VERIFICATION_START == event.type) { } else if (EventType.KEY_VERIFICATION_START == event.getClearType()) {
event.getClearContent().toModel<MessageVerificationStartContent>()?.let { event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
if (it.fromDevice != deviceId) { if (it.fromDevice != deviceId) {
// The verification is started from another device // The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
params.verificationService.onRoomRequestHandledByOtherDevice(event) params.verificationService.onRoomRequestHandledByOtherDevice(event)
} }
} }
} else if (EventType.KEY_VERIFICATION_READY == event.type) { } else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) {
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let { event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
if (it.fromDevice != deviceId) { if (it.fromDevice != deviceId) {
// The verification is started from another device // The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
params.verificationService.onRoomRequestHandledByOtherDevice(event) params.verificationService.onRoomRequestHandledByOtherDevice(event)
} }
} }
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) { } else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) {
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let { relatesToEventId?.let {
transactionsHandledByOtherDevice.remove(it) transactionsHandledByOtherDevice.remove(it)
params.verificationService.onRoomRequestHandledByOtherDevice(event) params.verificationService.onRoomRequestHandledByOtherDevice(event)
} }
@ -127,10 +130,9 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
return@forEach return@forEach
} }
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) {
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
// Ignore this event, it is directed to another of my devices // Ignore this event, it is directed to another of my devices
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ") Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ")
return@forEach return@forEach
} }
when (event.getClearType()) { when (event.getClearType()) {

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright (C) 2015 Square, Inc.
*
* 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.tools
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.math.ceil
/**
* HMAC-based Extract-and-Expand Key Derivation Function (HkdfSha256)
* [RFC-5869] https://tools.ietf.org/html/rfc5869
*/
object HkdfSha256 {
public fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray {
return expand(extract(salt, inputKeyMaterial), info, outputLength)
}
/**
* HkdfSha256-Extract(salt, IKM) -> PRK
*
* @param salt optional salt value (a non-secret random value);
* if not provided, it is set to a string of HashLen (size in octets) zeros.
* @param ikm input keying material
*/
private fun extract(salt: ByteArray?, ikm: ByteArray): ByteArray {
val mac = initMac(salt ?: ByteArray(HASH_LEN) { 0.toByte() })
return mac.doFinal(ikm)
}
/**
* HkdfSha256-Expand(PRK, info, L) -> OKM
*
* @param prk a pseudorandom key of at least HashLen bytes (usually, the output from the extract step)
* @param info optional context and application specific information (can be empty)
* @param outputLength length of output keying material in bytes (<= 255*HashLen)
* @return OKM output keying material
*/
private fun expand(prk: ByteArray, info: ByteArray = ByteArray(0), outputLength: Int): ByteArray {
require(outputLength <= 255 * HASH_LEN) { "outputLength must be less than or equal to 255*HashLen" }
/*
The output OKM is calculated as follows:
Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument;
N = ceil(L/HashLen)
T = T(1) | T(2) | T(3) | ... | T(N)
OKM = first L octets of T
where:
T(0) = empty string (zero length)
T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
...
*/
val n = ceil(outputLength.toDouble() / HASH_LEN.toDouble()).toInt()
var stepHash = ByteArray(0) // T(0) empty string (zero length)
val generatedBytes = ByteArrayOutputStream() // ByteBuffer.allocate(Math.multiplyExact(n, HASH_LEN))
val mac = initMac(prk)
for (roundNum in 1..n) {
mac.reset()
val t = ByteBuffer.allocate(stepHash.size + info.size + 1).apply {
put(stepHash)
put(info)
put(roundNum.toByte())
}
stepHash = mac.doFinal(t.array())
generatedBytes.write(stepHash)
}
return generatedBytes.toByteArray().sliceArray(0 until outputLength)
}
private fun initMac(secret: ByteArray): Mac {
val mac = Mac.getInstance(HASH_ALG)
mac.init(SecretKeySpec(secret, HASH_ALG))
return mac
}
private const val HASH_LEN = 32
private const val HASH_ALG = "HmacSHA256"
}

View File

@ -255,7 +255,7 @@ internal class DefaultVerificationService @Inject constructor(
} }
fun onRoomRequestHandledByOtherDevice(event: Event) { fun onRoomRequestHandledByOtherDevice(event: Event) {
val requestInfo = event.getClearContent().toModel<MessageRelationContent>() val requestInfo = event.content.toModel<MessageRelationContent>()
?: return ?: return
val requestId = requestInfo.relatesTo?.eventId ?: return val requestId = requestInfo.relatesTo?.eventId ?: return
getExistingVerificationRequestInRoom(event.roomId ?: "", requestId)?.let { getExistingVerificationRequestInRoom(event.roomId ?: "", requestId)?.let {
@ -465,7 +465,11 @@ internal class DefaultVerificationService @Inject constructor(
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
// If there is a corresponding request, we can auto accept // If there is a corresponding request, we can auto accept
// as we are the one requesting in first place (or we accepted the request) // as we are the one requesting in first place (or we accepted the request)
val autoAccept = getExistingVerificationRequest(otherUserId)?.any { it.transactionId == startReq.transactionID } // I need to check if the pending request was related to this device also
val autoAccept = getExistingVerificationRequest(otherUserId)?.any {
it.transactionId == startReq.transactionID
&& (it.requestInfo?.fromDevice == this.deviceId || it.readyInfo?.fromDevice == this.deviceId)
}
?: false ?: false
val tx = DefaultIncomingSASDefaultVerificationTransaction( val tx = DefaultIncomingSASDefaultVerificationTransaction(
// this, // this,
@ -1083,8 +1087,12 @@ internal class DefaultVerificationService @Inject constructor(
} }
.distinct() .distinct()
transport.sendVerificationRequest(methodValues, localID, otherUserId, null, targetDevices) { _, _ -> transport.sendVerificationRequest(methodValues, localID, otherUserId, null, targetDevices) { _, info ->
// Nothing special to do in to device mode // Nothing special to do in to device mode
updatePendingRequest(verificationRequest.copy(
// localId stays different
requestInfo = info
))
} }
requestsForUser.add(verificationRequest) requestsForUser.add(verificationRequest)

View File

@ -312,7 +312,7 @@ internal abstract class SASDefaultVerificationTransaction(
if (otherUserId == userId) { if (otherUserId == userId) {
// If me it's reasonable to sign and upload the device signature // If me it's reasonable to sign and upload the device signature
// Notice that i might not have the private keys, so may not be able to do it // Notice that i might not have the private keys, so may not be able to do it
crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback<Unit> { crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
Timber.w(failure, "## SAS Verification: Failed to sign new device $otherDeviceId") Timber.w(failure, "## SAS Verification: Failed to sign new device $otherDeviceId")
} }

View File

@ -222,7 +222,8 @@ internal class DefaultQrCodeVerificationTransaction(
private fun trust(canTrustOtherUserMasterKey: Boolean, toVerifyDeviceIds: List<String>) { private fun trust(canTrustOtherUserMasterKey: Boolean, toVerifyDeviceIds: List<String>) {
// If not me sign his MSK and upload the signature // If not me sign his MSK and upload the signature
if (otherUserId != userId && canTrustOtherUserMasterKey) { if (canTrustOtherUserMasterKey) {
if (otherUserId != userId) {
// we should trust this master key // we should trust this master key
// And check verification MSK -> SSK? // And check verification MSK -> SSK?
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> { crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
@ -230,12 +231,16 @@ internal class DefaultQrCodeVerificationTransaction(
Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId") Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId")
} }
}) })
} else {
// Mark my keys as trusted locally
crossSigningService.markMyMasterKeyAsTrusted()
}
} }
if (otherUserId == userId) { if (otherUserId == userId) {
// If me it's reasonable to sign and upload the device signature // If me it's reasonable to sign and upload the device signature
// Notice that i might not have the private keys, so may not be able to do it // Notice that i might not have the private keys, so may not be able to do it
crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback<Unit> { crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
Timber.w(failure, "## QR Verification: Failed to sign new device $otherDeviceId") Timber.w(failure, "## QR Verification: Failed to sign new device $otherDeviceId")
} }

View File

@ -31,6 +31,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let { val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let {
timelineEventMapper.map(it, buildReadReceipts = false) timelineEventMapper.map(it, buildReadReceipts = false)
} }
return RoomSummary( return RoomSummary(
roomId = roomSummaryEntity.roomId, roomId = roomSummaryEntity.roomId,
displayName = roomSummaryEntity.displayName ?: "", displayName = roomSummaryEntity.displayName ?: "",

View File

@ -140,6 +140,7 @@
<activity android:name=".features.qrcode.QrCodeScannerActivity" /> <activity android:name=".features.qrcode.QrCodeScannerActivity" />
<activity android:name=".features.crypto.quads.SharedSecureStorageActivity" />
<activity <activity
android:name="com.yalantis.ucrop.UCropActivity" android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />

View File

@ -26,6 +26,7 @@ import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.debug.DebugMenuActivity
import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeActivity
@ -151,6 +152,8 @@ interface ScreenComponent {
fun inject(deviceListBottomSheet: DeviceListBottomSheet) fun inject(deviceListBottomSheet: DeviceListBottomSheet)
fun inject(activity: SharedSecureStorageActivity)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(vectorComponent: VectorComponent, fun create(vectorComponent: VectorComponent,

View File

@ -0,0 +1,39 @@
/*
* 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.riotx.features.crypto.quads
import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.platform.WaitingViewData
sealed class SharedSecureStorageAction : VectorViewModelAction {
object TogglePasswordVisibility : SharedSecureStorageAction()
object Cancel : SharedSecureStorageAction()
data class SubmitPassphrase(val passphrase: String) : SharedSecureStorageAction()
}
sealed class SharedSecureStorageViewEvent : VectorViewEvents {
object Dismiss : SharedSecureStorageViewEvent()
data class FinishSuccess(val cypherResult: String) : SharedSecureStorageViewEvent()
data class Error(val message: String, val dismiss: Boolean = false) : SharedSecureStorageViewEvent()
data class InlineError(val message: String) : SharedSecureStorageViewEvent()
object ShowModalLoading : SharedSecureStorageViewEvent()
object HideModalLoading : SharedSecureStorageViewEvent()
data class UpdateLoadingState(val waitingData: WaitingViewData) : SharedSecureStorageViewEvent()
}

View File

@ -0,0 +1,129 @@
/*
* 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.riotx.features.crypto.quads
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.platform.SimpleFragmentActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity.*
import javax.inject.Inject
class SharedSecureStorageActivity : SimpleFragmentActivity() {
@Parcelize
data class Args(
val keyId: String?,
val requestedSecrets: List<String>,
val resultKeyStoreAlias: String
) : Parcelable
private val viewModel: SharedSecureStorageViewModel by viewModel()
@Inject lateinit var viewModelFactory: SharedSecureStorageViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE
if (isFirstCreation()) {
addFragment(R.id.container, SharedSecuredStoragePassphraseFragment::class.java)
}
viewModel.viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
observeViewEvents(it)
}
.disposeOnDestroy()
viewModel.subscribe(this) {
// renderState(it)
}
}
private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
when (it) {
is SharedSecureStorageViewEvent.Dismiss -> {
finish()
}
is SharedSecureStorageViewEvent.Error -> {
AlertDialog.Builder(this)
.setTitle(getString(R.string.dialog_title_error))
.setMessage(it.message)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
if (it.dismiss) {
finish()
}
}
.show()
}
is SharedSecureStorageViewEvent.ShowModalLoading -> {
showWaitingView()
}
is SharedSecureStorageViewEvent.HideModalLoading -> {
hideWaitingView()
}
is SharedSecureStorageViewEvent.UpdateLoadingState -> {
updateWaitingView(it.waitingData)
}
is SharedSecureStorageViewEvent.FinishSuccess -> {
val dataResult = Intent()
dataResult.putExtra(EXTRA_DATA_RESULT, it.cypherResult)
setResult(Activity.RESULT_OK, dataResult)
finish()
}
}
}
companion object {
const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT"
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
fun newIntent(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(MvRx.KEY_ARG, Args(
keyId,
requestedSecrets,
resultKeyStoreAlias
))
}
}
}
}

View File

@ -0,0 +1,165 @@
/*
* 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.riotx.features.crypto.quads
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.securestorage.IntegrityResult
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.resources.StringProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
data class SharedSecureStorageViewState(
val passphraseVisible: Boolean = false
) : MvRxState
class SharedSecureStorageViewModel @AssistedInject constructor(
@Assisted initialState: SharedSecureStorageViewState,
@Assisted val args: SharedSecureStorageActivity.Args,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<SharedSecureStorageViewState, SharedSecureStorageAction, SharedSecureStorageViewEvent>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: SharedSecureStorageViewState, args: SharedSecureStorageActivity.Args): SharedSecureStorageViewModel
}
init {
val isValid = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(args.requestedSecrets, args.keyId) is IntegrityResult.Success
if (!isValid) {
_viewEvents.post(
SharedSecureStorageViewEvent.Error(
stringProvider.getString(R.string.enter_secret_storage_invalid),
true
)
)
}
}
override fun handle(action: SharedSecureStorageAction) = withState {
when (action) {
is SharedSecureStorageAction.TogglePasswordVisibility -> handleTogglePasswordVisibility()
is SharedSecureStorageAction.Cancel -> handleCancel()
is SharedSecureStorageAction.SubmitPassphrase -> handleSubmitPassphrase(action)
}
}
private fun handleSubmitPassphrase(action: SharedSecureStorageAction.SubmitPassphrase) {
val decryptedSecretMap = HashMap<String, String>()
GlobalScope.launch(Dispatchers.IO) {
runCatching {
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
val passphrase = action.passphrase
val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
if (!keyInfoResult.isSuccess()) {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.Error("Cannot find ssss key"))
return@launch
}
val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
WaitingViewData(
message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
isIndeterminate = true
)
))
val keySpec = RawBytesKeySpec.fromPassphrase(
passphrase,
keyInfo.content.passphrase?.salt ?: "",
keyInfo.content.passphrase?.iterations ?: 0,
// TODO
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
WaitingViewData(
message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
isIndeterminate = false,
progress = progress,
progressTotal = total
)
))
}
}
)
withContext(Dispatchers.IO) {
args.requestedSecrets.forEach {
val res = awaitCallback<String> { callback ->
session.sharedSecretStorageService.getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec,
callback = callback)
}
decryptedSecretMap[it] = res
}
}
}.fold({
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
val safeForIntentCypher = ByteArrayOutputStream().also {
it.use {
session.securelyStoreObject(decryptedSecretMap as Map<String, String>, args.resultKeyStoreAlias, it)
}
}.toByteArray().toBase64NoPadding()
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
}, {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.InlineError(stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)))
})
}
}
private fun handleCancel() {
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
}
private fun handleTogglePasswordVisibility() {
setState {
copy(
passphraseVisible = !passphraseVisible
)
}
}
companion object : MvRxViewModelFactory<SharedSecureStorageViewModel, SharedSecureStorageViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: SharedSecureStorageViewState): SharedSecureStorageViewModel? {
val activity: SharedSecureStorageActivity = viewModelContext.activity()
val args: SharedSecureStorageActivity.Args = activity.intent.getParcelableExtra(MvRx.KEY_ARG)
return activity.viewModelFactory.create(state, args)
}
}
}

View File

@ -0,0 +1,116 @@
/*
* 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.riotx.features.crypto.quads
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.view.clicks
import com.jakewharton.rxbinding3.widget.editorActionEvents
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.*
import me.gujun.android.span.span
import java.util.concurrent.TimeUnit
class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_ssss_access_from_passphrase
val sharedViewModel: SharedSecureStorageViewModel by activityViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ssss_restore_with_passphrase_warning_text.text = span {
span(getString(R.string.enter_secret_storage_passphrase_warning)) {
textStyle = "bold"
}
+" "
+getString(R.string.enter_secret_storage_passphrase_warning_text)
}
ssss_restore_with_passphrase_warning_reason.text = getString(R.string.enter_secret_storage_passphrase_reason_verify)
ssss_passphrase_enter_edittext.editorActionEvents()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
submit()
}
}
.disposeOnDestroyView()
ssss_passphrase_enter_edittext.textChanges()
.subscribe {
ssss_passphrase_enter_til.error = null
ssss_passphrase_submit.isEnabled = it.isNotBlank()
}
.disposeOnDestroyView()
sharedViewModel.observeViewEvents {
when (it) {
is SharedSecureStorageViewEvent.InlineError -> {
ssss_passphrase_enter_til.error = it.message
}
}
}
ssss_passphrase_submit.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
submit()
}
.disposeOnDestroyView()
ssss_passphrase_cancel.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
sharedViewModel.handle(SharedSecureStorageAction.Cancel)
}
.disposeOnDestroyView()
ssss_view_show_password.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility)
}
.disposeOnDestroyView()
}
fun submit() {
val text = ssss_passphrase_enter_edittext.text.toString()
if (text.isBlank()) return // Should not reach this point as button disabled
ssss_passphrase_submit.isEnabled = false
sharedViewModel.handle(SharedSecureStorageAction.SubmitPassphrase(text))
}
override fun invalidate() = withState(sharedViewModel) { state ->
val shouldBeVisible = state.passphraseVisible
ssss_passphrase_enter_edittext.showPassword(shouldBeVisible)
ssss_view_show_password.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
}
}

View File

@ -28,4 +28,7 @@ sealed class VerificationAction : VectorViewModelAction {
data class SASMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction() data class SASMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction() data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
object GotItConclusion : VerificationAction() object GotItConclusion : VerificationAction()
object SkipVerification : VerificationAction()
object VerifyFromPassphrase : VerificationAction()
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
} }

View File

@ -15,26 +15,32 @@
*/ */
package im.vector.riotx.features.crypto.verification package im.vector.riotx.features.crypto.verification
import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import butterknife.BindView import butterknife.BindView
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_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.sas.VerificationTxState import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
@ -42,7 +48,6 @@ import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQ
import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_verification.*
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -54,10 +59,12 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
val otherUserId: String, val otherUserId: String,
val verificationId: String? = null, val verificationId: String? = null,
val roomId: String? = null, val roomId: String? = null,
// Special mode where UX should show loading wheel until other user sends a request/tx // Special mode where UX should show loading wheel until other session sends a request/tx
val waitForIncomingRequest: Boolean = false val selfVerificationMode: Boolean = false
) : Parcelable ) : Parcelable
override val showExpanded = true
@Inject @Inject
lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
@Inject @Inject
@ -86,14 +93,43 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
is VerificationBottomSheetViewEvents.Dismiss -> dismiss() is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
is VerificationBottomSheetViewEvents.AccessSecretStore -> {
startActivityForResult(SharedSecureStorageActivity.newIntent(
requireContext(),
null, // use default key
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME),
SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
), SECRET_REQUEST_CODE)
}
is VerificationBottomSheetViewEvents.ModalError -> {
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.dialog_title_error))
.setMessage(it.errorMessage)
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
Unit
}
}.exhaustive }.exhaustive
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) {
data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let {
viewModel.handle(VerificationAction.GotResultFromSsss(it, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
}
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
state.otherUserMxItem?.let { matrixItem -> state.otherUserMxItem?.let { matrixItem ->
if (state.isMe) { if (state.isMe) {
if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified) { if (state.sasTransactionState == VerificationTxState.Verified
|| state.qrTransactionState == VerificationTxState.Verified
|| state.verifiedFromPrivateKeys) {
otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted) otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted)
} else { } else {
otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning) otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning)
@ -113,6 +149,13 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }
} }
if (state.selfVerificationMode && state.verifiedFromPrivateKeys) {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
})
return@withState
}
// Did the request result in a SAS transaction? // Did the request result in a SAS transaction?
if (state.sasTransactionState != null) { if (state.sasTransactionState != null) {
when (state.sasTransactionState) { when (state.sasTransactionState) {
@ -183,7 +226,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }
// If it's an outgoing // If it's an outgoing
if (state.pendingRequest.invoke() == null || state.pendingRequest.invoke()?.isIncoming == false || state.waitForOtherUserMode) { if (state.pendingRequest.invoke() == null || state.pendingRequest.invoke()?.isIncoming == false || state.selfVerificationMode) {
Timber.v("## SAS show bottom sheet for outgoing request") Timber.v("## SAS show bottom sheet for outgoing request")
if (state.pendingRequest.invoke()?.isReady == true) { if (state.pendingRequest.invoke()?.isReady == true) {
Timber.v("## SAS show bottom sheet for outgoing and ready request") Timber.v("## SAS show bottom sheet for outgoing and ready request")
@ -214,12 +257,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) { private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
// We want to animate the bottomsheet bound changes childFragmentManager.commitTransaction {
bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
}
// Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate)
childFragmentManager.commitTransactionNow {
replace(R.id.bottomSheetFragmentContainer, replace(R.id.bottomSheetFragmentContainer,
fragmentClass.java, fragmentClass.java,
bundle, bundle,
@ -230,14 +268,28 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }
companion object { companion object {
fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null, waitForIncomingRequest: Boolean = false): VerificationBottomSheet {
const val SECRET_REQUEST_CODE = 101
fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null): VerificationBottomSheet {
return VerificationBottomSheet().apply { return VerificationBottomSheet().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs( putParcelable(MvRx.KEY_ARG, VerificationArgs(
otherUserId = otherUserId, otherUserId = otherUserId,
roomId = roomId, roomId = roomId,
verificationId = transactionId, verificationId = transactionId,
waitForIncomingRequest = waitForIncomingRequest selfVerificationMode = false
))
}
}
}
fun forSelfVerification(session: Session): VerificationBottomSheet {
return VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(
otherUserId = session.myUserId,
selfVerificationMode = true
)) ))
} }
} }

View File

@ -23,4 +23,6 @@ import im.vector.riotx.core.platform.VectorViewEvents
*/ */
sealed class VerificationBottomSheetViewEvents : VectorViewEvents { sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
object Dismiss : VerificationBottomSheetViewEvents() object Dismiss : VerificationBottomSheetViewEvents()
object AccessSecretStore : VerificationBottomSheetViewEvents()
data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents()
} }

View File

@ -28,6 +28,9 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_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.sas.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
@ -39,9 +42,12 @@ import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import timber.log.Timber
data class VerificationBottomSheetViewState( data class VerificationBottomSheetViewState(
val otherUserMxItem: MatrixItem? = null, val otherUserMxItem: MatrixItem? = null,
@ -52,7 +58,8 @@ data class VerificationBottomSheetViewState(
val qrTransactionState: VerificationTxState? = null, val qrTransactionState: VerificationTxState? = null,
val transactionId: String? = null, val transactionId: String? = null,
// true when we display the loading and we wait for the other (incoming request) // true when we display the loading and we wait for the other (incoming request)
val waitForOtherUserMode: Boolean = false, val selfVerificationMode: Boolean = false,
val verifiedFromPrivateKeys: Boolean = false,
val isMe: Boolean = false val isMe: Boolean = false
) : MvRxState ) : MvRxState
@ -67,10 +74,10 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
val userItem = session.getUser(args.otherUserId) val userItem = session.getUser(args.otherUserId)
val isWaitingForOtherMode = args.waitForIncomingRequest val selfVerificationMode = args.selfVerificationMode
var autoReady = false var autoReady = false
val pr = if (isWaitingForOtherMode) { val pr = if (selfVerificationMode) {
// See if active tx for this user and take it // See if active tx for this user and take it
session.cryptoService().verificationService().getExistingVerificationRequest(args.otherUserId) session.cryptoService().verificationService().getExistingVerificationRequest(args.otherUserId)
@ -100,7 +107,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
qrTransactionState = qrTx?.state, qrTransactionState = qrTx?.state,
transactionId = pr?.transactionId ?: args.verificationId, transactionId = pr?.transactionId ?: args.verificationId,
pendingRequest = if (pr != null) Success(pr) else Uninitialized, pendingRequest = if (pr != null) Success(pr) else Uninitialized,
waitForOtherUserMode = isWaitingForOtherMode, selfVerificationMode = selfVerificationMode,
roomId = args.roomId, roomId = args.roomId,
isMe = args.otherUserId == session.myUserId isMe = args.otherUserId == session.myUserId
) )
@ -250,6 +257,46 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
is VerificationAction.GotItConclusion -> { is VerificationAction.GotItConclusion -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
} }
is VerificationAction.SkipVerification -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
is VerificationAction.VerifyFromPassphrase -> {
_viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore)
}
is VerificationAction.GotResultFromSsss -> {
try {
action.cypherData.fromBase64NoPadding().inputStream().use { ins ->
val res = session.loadSecureSecret<Map<String, String>>(ins, action.alias)
val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys(
res?.get(MASTER_KEY_SSSS_NAME),
res?.get(USER_SIGNING_KEY_SSSS_NAME),
res?.get(SELF_SIGNING_KEY_SSSS_NAME)
)
if (trustResult.isVerified()) {
// Sign this device and upload the signature
session.sessionParams.credentials.deviceId?.let { deviceId ->
session.cryptoService()
.crossSigningService().trustDevice(deviceId, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.w("Failed to sign my device after recovery", failure)
}
})
}
setState {
copy(verifiedFromPrivateKeys = true)
}
} else {
// POP UP something
_viewEvents.post(VerificationBottomSheetViewEvents.ModalError("Failed to import keys"))
}
}
} catch (failure: Throwable) {
_viewEvents.post(VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage))
}
Unit
}
}.exhaustive }.exhaustive
} }
@ -258,7 +305,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
} }
override fun transactionUpdated(tx: VerificationTransaction) = withState { state -> override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
if (state.waitForOtherUserMode && state.transactionId == null) { if (state.selfVerificationMode && state.transactionId == null) {
// is this an incoming with that user // is this an incoming with that user
if (tx.isIncoming && tx.otherUserId == state.otherUserMxItem?.id) { if (tx.isIncoming && tx.otherUserId == state.otherUserMxItem?.id) {
// Also auto accept incoming if needed! // Also auto accept incoming if needed!
@ -308,7 +355,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state -> override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
if (state.waitForOtherUserMode && state.pendingRequest.invoke() == null && state.transactionId == null) { if (state.selfVerificationMode && state.pendingRequest.invoke() == null && state.transactionId == null) {
// is this an incoming with that user // is this an incoming with that user
if (pr.isIncoming && pr.otherUserId == state.otherUserMxItem?.id) { if (pr.isIncoming && pr.otherUserId == state.otherUserMxItem?.id) {
if (!pr.isReady) { if (!pr.isReady) {

View File

@ -50,7 +50,7 @@ class VerificationRequestController @Inject constructor(
val state = viewState ?: return val state = viewState ?: return
val matrixItem = viewState?.otherUserMxItem ?: return val matrixItem = viewState?.otherUserMxItem ?: return
if (state.waitForOtherUserMode) { if (state.selfVerificationMode) {
bottomSheetVerificationNoticeItem { bottomSheetVerificationNoticeItem {
id("notice") id("notice")
notice(stringProvider.getString(R.string.verification_open_other_to_verify)) notice(stringProvider.getString(R.string.verification_open_other_to_verify))
@ -62,7 +62,26 @@ class VerificationRequestController @Inject constructor(
bottomSheetVerificationWaitingItem { bottomSheetVerificationWaitingItem {
id("waiting") id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) title(stringProvider.getString(R.string.verification_request_waiting, matrixItem.getBestName()))
}
bottomSheetVerificationActionItem {
id("passphrase")
title(stringProvider.getString(R.string.verification_cannot_access_other_session))
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
subTitle(stringProvider.getString(R.string.verification_use_passphrase))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener?.onClickRecoverFromPassphrase() }
}
bottomSheetVerificationActionItem {
id("skip")
title(stringProvider.getString(R.string.skip))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
// subTitle(stringProvider.getString(R.string.verification_use_passphrase))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener { listener?.onClickDismiss() }
} }
} else { } else {
val styledText = matrixItem.let { val styledText = matrixItem.let {
@ -112,5 +131,7 @@ class VerificationRequestController @Inject constructor(
interface Listener { interface Listener {
fun onClickOnVerificationStart() fun onClickOnVerificationStart()
fun onClickRecoverFromPassphrase()
fun onClickDismiss()
} }
} }

View File

@ -61,4 +61,12 @@ class VerificationRequestFragment @Inject constructor(
viewModel.handle(VerificationAction.RequestVerificationByDM(otherUserId, state.roomId)) viewModel.handle(VerificationAction.RequestVerificationByDM(otherUserId, state.roomId))
} }
} }
override fun onClickRecoverFromPassphrase() {
viewModel.handle(VerificationAction.VerifyFromPassphrase)
}
override fun onClickDismiss() {
viewModel.handle(VerificationAction.SkipVerification)
}
} }

View File

@ -150,34 +150,17 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
PopupAlertManager.postVectorAlert( PopupAlertManager.postVectorAlert(
PopupAlertManager.VectorAlert( PopupAlertManager.VectorAlert(
uid = "completeSecurity", uid = "completeSecurity",
title = getString(R.string.crosssigning_verify_this_session), title = getString(R.string.new_signin),
description = getString(R.string.crosssigning_other_user_not_trust), description = getString(R.string.complete_security),
iconId = R.drawable.ic_shield_warning iconId = R.drawable.ic_shield_warning
).apply { ).apply {
colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent) colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_destructive_accent)
contentAction = Runnable { contentAction = Runnable {
Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let { (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
it.navigator.waitSessionVerification(it) it.navigator.waitSessionVerification(it)
} }
} }
} dismissedAction = Runnable {}
dismissedAction = Runnable {
// tx.cancel()
}
addButton(
getString(R.string.later),
Runnable {
}
)
addButton(
getString(R.string.verification_profile_verify),
Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
it.navigator.waitSessionVerification(it)
}
}
)
} }
) )
} }

View File

@ -95,12 +95,8 @@ class DefaultNavigator @Inject constructor(
override fun waitSessionVerification(context: Context) { override fun waitSessionVerification(context: Context) {
val session = sessionHolder.getSafeActiveSession() ?: return val session = sessionHolder.getSafeActiveSession() ?: return
if (context is VectorBaseActivity) { if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs( VerificationBottomSheet.forSelfVerification(session)
roomId = null, .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
otherUserId = session.myUserId,
waitForIncomingRequest = true
).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
} }
} }

View File

@ -36,6 +36,7 @@ class CrossSigningEpoxyController @Inject constructor(
interface InteractionListener { interface InteractionListener {
fun onInitializeCrossSigningKeys() fun onInitializeCrossSigningKeys()
fun onResetCrossSigningKeys() fun onResetCrossSigningKeys()
fun verifySession()
} }
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
@ -77,12 +78,23 @@ class CrossSigningEpoxyController @Inject constructor(
interactionListener?.onResetCrossSigningKeys() interactionListener?.onResetCrossSigningKeys()
} }
} }
}
} else if (data.xSigningIsEnableInAccount) { } else if (data.xSigningIsEnableInAccount) {
genericItem { genericItem {
id("enable") id("enable")
titleIconResourceId(R.drawable.ic_shield_black) titleIconResourceId(R.drawable.ic_shield_black)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted)) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
} }
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.complete_security))
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
listener {
interactionListener?.verifySession()
}
}
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("resetkeys") id("resetkeys")
title("Reset keys") title("Reset keys")
@ -93,7 +105,6 @@ class CrossSigningEpoxyController @Inject constructor(
interactionListener?.onResetCrossSigningKeys() interactionListener?.onResetCrossSigningKeys()
} }
} }
}
} else { } else {
genericItem { genericItem {
id("not") id("not")

View File

@ -54,6 +54,11 @@ class CrossSigningSettingsFragment @Inject constructor(
is CrossSigningSettingsViewEvents.RequestPassword -> { is CrossSigningSettingsViewEvents.RequestPassword -> {
requestPassword() requestPassword()
} }
CrossSigningSettingsViewEvents.VerifySession -> {
(requireActivity() as? VectorBaseActivity)?.let { activity ->
activity.navigator.waitSessionVerification(activity)
}
}
}.exhaustive }.exhaustive
} }
} }
@ -93,6 +98,10 @@ class CrossSigningSettingsFragment @Inject constructor(
viewModel.handle(CrossSigningAction.InitializeCrossSigning) viewModel.handle(CrossSigningAction.InitializeCrossSigning)
} }
override fun verifySession() {
viewModel.handle(CrossSigningAction.VerifySession)
}
override fun onResetCrossSigningKeys() { override fun onResetCrossSigningKeys() {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_confirmation) .setTitle(R.string.dialog_title_confirmation)

View File

@ -25,4 +25,5 @@ sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents() data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
object RequestPassword : CrossSigningSettingsViewEvents() object RequestPassword : CrossSigningSettingsViewEvents()
object VerifySession : CrossSigningSettingsViewEvents()
} }

View File

@ -45,6 +45,7 @@ data class CrossSigningSettingsViewState(
sealed class CrossSigningAction : VectorViewModelAction { sealed class CrossSigningAction : VectorViewModelAction {
object InitializeCrossSigning : CrossSigningAction() object InitializeCrossSigning : CrossSigningAction()
object VerifySession : CrossSigningAction()
data class PasswordEntered(val password: String) : CrossSigningAction() data class PasswordEntered(val password: String) : CrossSigningAction()
} }
@ -88,6 +89,9 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
password = action.password password = action.password
)) ))
} }
CrossSigningAction.VerifySession -> {
_viewEvents.post(CrossSigningSettingsViewEvents.VerifySession)
}
}.exhaustive }.exhaustive
} }

View File

@ -60,7 +60,7 @@
android:id="@+id/bottomSheetFragmentContainer" android:id="@+id/bottomSheetFragmentContainer"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/verificationRequestAvatar" /> app:layout_constraintTop_toBottomOf="@+id/verificationRequestAvatar" />

View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ssss__root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ssss_shield"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="16dp"
android:src="@drawable/key_big"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_passphrase"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_passphrase" />
<TextView
android:id="@+id/ssss_restore_with_passphrase"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="16dp"
android:text="@string/enter_secret_storage_passphrase"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/ssss_shield"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/ssss_restore_with_passphrase_warning_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase"
tools:text="@string/enter_secret_storage_passphrase_warning_text" />
<TextView
android:id="@+id/ssss_restore_with_passphrase_warning_reason"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase_warning_text"
tools:text="@string/enter_secret_storage_passphrase_reason_verify" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ssss_passphrase_enter_til"
style="@style/VectorTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
app:errorEnabled="true"
app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase_warning_reason">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ssss_passphrase_enter_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/passphrase_enter_passphrase"
android:imeOptions="actionDone"
android:maxLines="3"
android:singleLine="false"
android:textColor="?android:textColorPrimary"
tools:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/ssss_view_show_password"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye_black"
android:tint="?colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/ssss_passphrase_enter_til"
app:layout_constraintTop_toTopOf="@+id/ssss_passphrase_enter_til" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_passphrase_submit"
style="@style/VectorButtonStylePositive"
android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:minWidth="200dp"
android:text="@string/_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_passphrase_cancel"
style="@style/VectorButtonStyleDestructive"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:minWidth="200dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_submit" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -2140,7 +2140,6 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.</string>
<item quantity="other">%d saio aktibo</item> <item quantity="other">%d saio aktibo</item>
</plurals> </plurals>
<string name="crosssigning_verify_this_session">Egiaztatu saio hau</string>
<string name="crosssigning_other_user_not_trust">Beste erabiltzaile batzuk ez fidagarritzat jo lezakete</string> <string name="crosssigning_other_user_not_trust">Beste erabiltzaile batzuk ez fidagarritzat jo lezakete</string>
<string name="complete_security">Bete segurtasuna</string> <string name="complete_security">Bete segurtasuna</string>

View File

@ -2148,7 +2148,6 @@ Si vous navez pas configuré de nouvelle méthode de récupération, un attaq
<item quantity="other">%d sessions actives</item> <item quantity="other">%d sessions actives</item>
</plurals> </plurals>
<string name="crosssigning_verify_this_session">Vérifier cette session</string>
<string name="crosssigning_other_user_not_trust">Les autres utilisateurs ne lui font peut-être pas confiance</string> <string name="crosssigning_other_user_not_trust">Les autres utilisateurs ne lui font peut-être pas confiance</string>
<string name="complete_security">Compléter la sécurité</string> <string name="complete_security">Compléter la sécurité</string>

View File

@ -2143,7 +2143,6 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
<item quantity="other">%d munkamenet használatban</item> <item quantity="other">%d munkamenet használatban</item>
</plurals> </plurals>
<string name="crosssigning_verify_this_session">Munkamenet ellenőrzése</string>
<string name="crosssigning_other_user_not_trust">Más felhasználók lehet, hogy nem bíznak benne</string> <string name="crosssigning_other_user_not_trust">Más felhasználók lehet, hogy nem bíznak benne</string>
<string name="complete_security">Biztonság beállítása</string> <string name="complete_security">Biztonság beállítása</string>

View File

@ -2193,7 +2193,6 @@
<item quantity="other">%d sessioni attive</item> <item quantity="other">%d sessioni attive</item>
</plurals> </plurals>
<string name="crosssigning_verify_this_session">Verifica questa sessione</string>
<string name="crosssigning_other_user_not_trust">Gli altri utenti potrebbero non fidarsi</string> <string name="crosssigning_other_user_not_trust">Gli altri utenti potrebbero non fidarsi</string>
<string name="complete_security">Completa la sicurezza</string> <string name="complete_security">Completa la sicurezza</string>

View File

@ -2062,7 +2062,6 @@ Që të garantoni se sju shpëton gjë, thjesht mbajeni të aktivizuar mekani
<item quantity="other">%d sesione aktive</item> <item quantity="other">%d sesione aktive</item>
</plurals> </plurals>
<string name="crosssigning_verify_this_session">Verifikoni këtë sesion</string>
<string name="crosssigning_other_user_not_trust">Përdorues të tjerë mund të mos e besojnë</string> <string name="crosssigning_other_user_not_trust">Përdorues të tjerë mund të mos e besojnë</string>
<string name="complete_security">Siguri e Plotë</string> <string name="complete_security">Siguri e Plotë</string>

View File

@ -2093,7 +2093,6 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
<item quantity="other">%d 活躍的工作階段</item> <item quantity="other">%d 活躍的工作階段</item>
</plurals> </plurals>
<string name="crosssigning_verify_this_session">驗證此工作階段</string>
<string name="crosssigning_other_user_not_trust">其他使用者可能不會信任它</string> <string name="crosssigning_other_user_not_trust">其他使用者可能不會信任它</string>
<string name="complete_security">全面的安全性</string> <string name="complete_security">全面的安全性</string>

View File

@ -2121,11 +2121,10 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<item quantity="other">%d active sessions</item> <item quantity="other">%d active sessions</item>
</plurals> </plurals>
<string name="crosssigning_verify_this_session">Verify this session</string>
<string name="crosssigning_other_user_not_trust">Other users may not trust it</string> <string name="crosssigning_other_user_not_trust">Other users may not trust it</string>
<string name="complete_security">Complete Security</string> <string name="complete_security">Complete Security</string>
<string name="verification_open_other_to_verify">Open an existing session &amp; use it to verify this one, granting it access to encrypted messages. If you cant access one, use your recovery key or passphrase.</string> <string name="verification_open_other_to_verify">Open an existing session &amp; use it to verify this one, granting it access to encrypted messages.</string>
<string name="verification_profile_verify">Verify</string> <string name="verification_profile_verify">Verify</string>

View File

@ -18,6 +18,19 @@
</plurals> </plurals>
<string name="poll_item_selected_aria">Selected Option</string> <string name="poll_item_selected_aria">Selected Option</string>
<string name="command_description_poll">Creates a simple poll</string> <string name="command_description_poll">Creates a simple poll</string>
<string name="verification_cannot_access_other_session">Cant access an existing session?</string>
<string name="verification_use_passphrase">Use your recovery key or passphrase</string>
<string name="new_signin">New Sign In</string>
<string name="enter_secret_storage_invalid">Cannot find secrets in storage</string>
<string name="enter_secret_storage_passphrase">Enter secret storage passphrase</string>
<string name="enter_secret_storage_passphrase_warning">Warning:</string>
<string name="enter_secret_storage_passphrase_warning_text">You should only access secret storage from a trusted device</string>
<string name="enter_secret_storage_passphrase_reason_verify">Access your secure message history and your cross-signing identity for verifying other sessions by entering your passphrase</string>
<!-- END Strings added by Valere --> <!-- END Strings added by Valere -->