Merge pull request #1015 from vector-im/feature/new_signin_passphrase
Feature/new signin passphrase
This commit is contained in:
commit
b7cf7e06a7
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
@ -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,11 +57,13 @@ 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,
|
||||||
otherDeviceId: String,
|
otherDeviceId: String,
|
||||||
|
@ -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"
|
@ -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 {
|
||||||
/**
|
/**
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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?
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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,16 +227,18 @@ 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())
|
||||||
it[userId] = (it[userId]
|
.also {
|
||||||
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
|
it[userId] = (it[userId]
|
||||||
}
|
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
|
||||||
|
}
|
||||||
myDevice.copy(signatures = updateSignatures).let {
|
myDevice.copy(signatures = updateSignatures).let {
|
||||||
uploadSignatureQueryBuilder.withDeviceInfo(it)
|
uploadSignatureQueryBuilder.withDeviceInfo(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
|
@ -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,29 +99,38 @@ 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 ->
|
||||||
.findAll()
|
RoomMemberSummaryEntity.where(realm, roomId)
|
||||||
.let { results ->
|
.findAll()
|
||||||
map[roomId] = results.map { it.userId }
|
.let { results ->
|
||||||
}
|
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 {
|
||||||
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList))
|
withContext(coroutineDispatchers.crypto) {
|
||||||
BACKGROUND_HANDLER.post {
|
try {
|
||||||
backgroundSessionRealm.get()?.executeTransaction { realm ->
|
// Can throw if the crypto database has been closed in between, in this case log and ignore?
|
||||||
roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust)
|
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList))
|
||||||
|
BACKGROUND_HANDLER.post {
|
||||||
|
backgroundSessionRealm.get()?.executeTransaction { realm ->
|
||||||
|
roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,24 +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()) {
|
keys.forEach {
|
||||||
// use default key
|
val keyId = it.keyId
|
||||||
when (val key = getDefaultKey()) {
|
// encrypt the content
|
||||||
|
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[key.keyInfo.id] = 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 ?: ""))
|
||||||
@ -217,34 +204,6 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
keys.forEach {
|
|
||||||
val keyId = it
|
|
||||||
// encrypt the content
|
|
||||||
when (val key = getKey(keyId)) {
|
|
||||||
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[keyId] = 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
accountDataService.updateAccountData(
|
accountDataService.updateAccountData(
|
||||||
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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?
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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()) {
|
||||||
|
@ -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"
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -222,20 +222,25 @@ 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) {
|
||||||
// we should trust this master key
|
if (otherUserId != userId) {
|
||||||
// And check verification MSK -> SSK?
|
// we should trust this master key
|
||||||
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
|
// And check verification MSK -> SSK?
|
||||||
override fun onFailure(failure: Throwable) {
|
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
|
||||||
Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId")
|
override fun onFailure(failure: Throwable) {
|
||||||
}
|
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")
|
||||||
}
|
}
|
||||||
|
@ -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 ?: "",
|
||||||
|
@ -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" />
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -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
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
@ -85,15 +92,44 @@ 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
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,21 +78,31 @@ class CrossSigningEpoxyController @Inject constructor(
|
|||||||
interactionListener?.onResetCrossSigningKeys()
|
interactionListener?.onResetCrossSigningKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (data.xSigningIsEnableInAccount) {
|
}
|
||||||
genericItem {
|
} else if (data.xSigningIsEnableInAccount) {
|
||||||
id("enable")
|
genericItem {
|
||||||
titleIconResourceId(R.drawable.ic_shield_black)
|
id("enable")
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
|
titleIconResourceId(R.drawable.ic_shield_black)
|
||||||
|
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 {
|
}
|
||||||
id("resetkeys")
|
bottomSheetVerificationActionItem {
|
||||||
title("Reset keys")
|
id("resetkeys")
|
||||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
title("Reset keys")
|
||||||
iconRes(R.drawable.ic_arrow_right)
|
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
listener {
|
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
interactionListener?.onResetCrossSigningKeys()
|
listener {
|
||||||
}
|
interactionListener?.onResetCrossSigningKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
|
||||||
|
@ -2148,7 +2148,6 @@ Si vous n’avez 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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -2062,7 +2062,6 @@ Që të garantoni se s’ju 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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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 & use it to verify this one, granting it access to encrypted messages. If you can’t access one, use your recovery key or passphrase.</string>
|
<string name="verification_open_other_to_verify">Open an existing session & 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>
|
||||||
|
@ -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">Can‘t 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 -->
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user