SSSS service + test
This commit is contained in:
parent
bf06b57bad
commit
108ebea84e
@ -31,7 +31,7 @@ import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
|
||||
@ -123,7 +123,7 @@ class RxSession(private val session: Session) {
|
||||
}
|
||||
}
|
||||
|
||||
fun liveAccountData(filter: List<String>): Observable<List<UserAccountData>> {
|
||||
fun liveAccountData(filter: List<String>): Observable<List<UserAccountDataEvent>> {
|
||||
return session.getLiveAccountData(filter).asObservable()
|
||||
.startWithCallable {
|
||||
session.getAccountData(filter)
|
||||
|
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.ssss
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
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.KeySigner
|
||||
import im.vector.matrix.android.api.session.securestorage.SSSSKeyCreationInfo
|
||||
import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.SessionTestParams
|
||||
import im.vector.matrix.android.common.TestConstants
|
||||
import im.vector.matrix.android.common.TestMatrixCallback
|
||||
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecureStorage
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
class QuadSTests : InstrumentedTest {
|
||||
|
||||
private val mTestHelper = CommonTestHelper(context())
|
||||
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
|
||||
|
||||
@Test
|
||||
fun test_Generate4SKey() {
|
||||
|
||||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
|
||||
val aliceLatch = CountDownLatch(1)
|
||||
|
||||
val quadS = aliceSession.sharedSecretStorageService
|
||||
|
||||
val emptyKeySigner = object : KeySigner {
|
||||
override fun sign(canonicalJson: String): Map<String, Map<String, String>>? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
var recoveryKey: String? = null
|
||||
|
||||
val TEST_KEY_ID = "my.test.Key"
|
||||
|
||||
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner,
|
||||
object : MatrixCallback<SSSSKeyCreationInfo> {
|
||||
|
||||
override fun onSuccess(data: SSSSKeyCreationInfo) {
|
||||
recoveryKey = data.recoveryKey
|
||||
aliceLatch.countDown()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Assert.fail("onFailure " + failure.localizedMessage)
|
||||
aliceLatch.countDown()
|
||||
}
|
||||
})
|
||||
|
||||
mTestHelper.await(aliceLatch)
|
||||
|
||||
// Assert Account data is updated
|
||||
val accountDataLock = CountDownLatch(1)
|
||||
var accountData: UserAccountDataEvent? = null
|
||||
|
||||
val liveAccountData = runBlocking(Dispatchers.Main) {
|
||||
aliceSession.getLiveAccountData("m.secret_storage.key.$TEST_KEY_ID")
|
||||
}
|
||||
val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
|
||||
if (t?.getOrNull()?.type == "m.secret_storage.key.$TEST_KEY_ID") {
|
||||
accountData = t.getOrNull()
|
||||
accountDataLock.countDown()
|
||||
}
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) }
|
||||
|
||||
mTestHelper.await(accountDataLock)
|
||||
|
||||
Assert.assertNotNull("Key should be stored in account data", accountData)
|
||||
val parsed = SecretStorageKeyContent.fromJson(accountData!!.content)
|
||||
Assert.assertNotNull("Key Content cannot be parsed", parsed)
|
||||
Assert.assertEquals("Unexpected Algorithm", DefaultSharedSecureStorage.ALGORITHM_CURVE25519_AES_SHA2, parsed!!.algorithm)
|
||||
Assert.assertEquals("Unexpected key name", "Test Key", parsed.name)
|
||||
Assert.assertNull("Key was not generated from passphrase", parsed.passphrase)
|
||||
Assert.assertNotNull("Pubkey should be defined", parsed.publicKey)
|
||||
|
||||
val privateKeySpec = Curve25519AesSha2KeySpec.fromRecoveryKey(recoveryKey!!)
|
||||
DefaultSharedSecureStorage.withOlmDecryption { olmPkDecryption ->
|
||||
val pubKey = olmPkDecryption.setPrivateKey(privateKeySpec!!.privateKey)
|
||||
Assert.assertEquals("Unexpected Public Key", pubKey, parsed.publicKey)
|
||||
}
|
||||
|
||||
// Set as default key
|
||||
quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback<Unit> {})
|
||||
|
||||
var defaultKeyAccountData: UserAccountDataEvent? = null
|
||||
val defaultDataLock = CountDownLatch(1)
|
||||
|
||||
val liveDefAccountData = runBlocking(Dispatchers.Main) {
|
||||
aliceSession.getLiveAccountData(DefaultSharedSecureStorage.DEFAULT_KEY_ID)
|
||||
}
|
||||
val accountDefDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
|
||||
if (t?.getOrNull()?.type == DefaultSharedSecureStorage.DEFAULT_KEY_ID) {
|
||||
defaultKeyAccountData = t.getOrNull()!!
|
||||
defaultDataLock.countDown()
|
||||
}
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main) { liveDefAccountData.observeForever(accountDefDataObserver) }
|
||||
|
||||
mTestHelper.await(defaultDataLock)
|
||||
|
||||
|
||||
Assert.assertNotNull(defaultKeyAccountData?.content)
|
||||
Assert.assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key"))
|
||||
|
||||
|
||||
mTestHelper.signout(aliceSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_StoreSecret() {
|
||||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
val keyId = "My.Key"
|
||||
val info = generatedSecret(aliceSession, keyId, true)
|
||||
|
||||
// Store a secret
|
||||
|
||||
val storeCountDownLatch = CountDownLatch(1)
|
||||
val clearSecret = Base64.encodeToString("42".toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
aliceSession.sharedSecretStorageService.storeSecret(
|
||||
"secret.of.life",
|
||||
clearSecret,
|
||||
null, // default key
|
||||
TestMatrixCallback(storeCountDownLatch)
|
||||
)
|
||||
|
||||
val secretAccountData = assertAccountData(aliceSession,"secret.of.life" )
|
||||
|
||||
val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*,*>
|
||||
Assert.assertNotNull("Element should be encrypted", encryptedContent)
|
||||
Assert.assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId))
|
||||
|
||||
val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId))
|
||||
Assert.assertNotNull(secret?.ciphertext)
|
||||
Assert.assertNotNull(secret?.mac)
|
||||
Assert.assertNotNull(secret?.ephemeral)
|
||||
|
||||
// Try to decrypt??
|
||||
|
||||
val keySpec = Curve25519AesSha2KeySpec.fromRecoveryKey(info.recoveryKey)
|
||||
|
||||
var decryptedSecret: String? = null
|
||||
|
||||
val decryptCountDownLatch = CountDownLatch(1)
|
||||
aliceSession.sharedSecretStorageService.getSecret("secret.of.life" ,
|
||||
null, //default key
|
||||
keySpec!!,
|
||||
null,
|
||||
object : MatrixCallback<String> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
fail("Fail to decrypt -> " +failure.localizedMessage)
|
||||
decryptCountDownLatch.countDown()
|
||||
}
|
||||
|
||||
override fun onSuccess(data: String) {
|
||||
decryptedSecret = data
|
||||
decryptCountDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
)
|
||||
mTestHelper.await(decryptCountDownLatch)
|
||||
|
||||
|
||||
Assert.assertEquals("Secret mismatch", clearSecret, decryptedSecret)
|
||||
mTestHelper.signout(aliceSession)
|
||||
|
||||
}
|
||||
|
||||
private fun assertAccountData(session: Session, type: String): UserAccountDataEvent {
|
||||
val accountDataLock = CountDownLatch(1)
|
||||
var accountData: UserAccountDataEvent? = null
|
||||
|
||||
val liveAccountData = runBlocking(Dispatchers.Main) {
|
||||
session.getLiveAccountData(type)
|
||||
}
|
||||
val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
|
||||
if (t?.getOrNull()?.type == type) {
|
||||
accountData = t.getOrNull()
|
||||
accountDataLock.countDown()
|
||||
}
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) }
|
||||
mTestHelper.await(accountDataLock)
|
||||
|
||||
Assert.assertNotNull("Account Data type:$type should be found", accountData)
|
||||
|
||||
return accountData!!
|
||||
}
|
||||
|
||||
private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SSSSKeyCreationInfo {
|
||||
|
||||
val quadS = session.sharedSecretStorageService
|
||||
|
||||
val emptyKeySigner = object : KeySigner {
|
||||
override fun sign(canonicalJson: String): Map<String, Map<String, String>>? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
var creationInfo: SSSSKeyCreationInfo? = null
|
||||
|
||||
val generateLatch = CountDownLatch(1)
|
||||
|
||||
quadS.generateKey(keyId, keyId, emptyKeySigner,
|
||||
object : MatrixCallback<SSSSKeyCreationInfo> {
|
||||
|
||||
override fun onSuccess(data: SSSSKeyCreationInfo) {
|
||||
creationInfo = data
|
||||
generateLatch.countDown()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Assert.fail("onFailure " + failure.localizedMessage)
|
||||
generateLatch.countDown()
|
||||
}
|
||||
})
|
||||
|
||||
mTestHelper.await(generateLatch)
|
||||
|
||||
Assert.assertNotNull(creationInfo)
|
||||
|
||||
assertAccountData(session, "m.secret_storage.key.$keyId")
|
||||
if (asDefault) {
|
||||
val setDefaultLatch = CountDownLatch(1)
|
||||
quadS.setDefaultKey(keyId, TestMatrixCallback(setDefaultLatch))
|
||||
mTestHelper.await(setDefaultLatch)
|
||||
assertAccountData(session, DefaultSharedSecureStorage.DEFAULT_KEY_ID)
|
||||
}
|
||||
|
||||
return creationInfo!!
|
||||
}
|
||||
}
|
@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.pushers.PushersService
|
||||
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
import im.vector.matrix.android.api.session.signout.SignOutService
|
||||
import im.vector.matrix.android.api.session.sync.FilterService
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
@ -161,4 +162,6 @@ interface Session :
|
||||
*/
|
||||
fun onGlobalError(globalError: GlobalError)
|
||||
}
|
||||
|
||||
val sharedSecretStorageService: SharedSecretStorageService
|
||||
}
|
||||
|
@ -19,17 +19,17 @@ package im.vector.matrix.android.api.session.accountdata
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
|
||||
interface AccountDataService {
|
||||
|
||||
fun getAccountData(type: String): UserAccountData?
|
||||
fun getAccountData(type: String): UserAccountDataEvent?
|
||||
|
||||
fun getLiveAccountData(type: String): LiveData<Optional<UserAccountData>>
|
||||
fun getLiveAccountData(type: String): LiveData<Optional<UserAccountDataEvent>>
|
||||
|
||||
fun getAccountData(filterType: List<String>): List<UserAccountData>
|
||||
fun getAccountData(filterType: List<String>): List<UserAccountDataEvent>
|
||||
|
||||
fun getLiveAccountData(filterType: List<String>): LiveData<List<UserAccountData>>
|
||||
fun getLiveAccountData(filterType: List<String>): LiveData<List<UserAccountDataEvent>>
|
||||
|
||||
fun updateAccountData(type: String, data: Any, callback: MatrixCallback<Unit>? = null)
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
|
||||
/**
|
||||
* The account_data will have an encrypted property that is a map from key ID to an object.
|
||||
* The algorithm from the m.secret_storage.key.[key ID] data for the given key defines how the other properties are interpreted,
|
||||
* though it's expected that most encryption schemes would have ciphertext and mac properties,
|
||||
* where the ciphertext property is the unpadded base64-encoded ciphertext, and the mac is used to ensure the integrity of the data.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EncryptedSecretContent(
|
||||
/** unpadded base64-encoded ciphertext */
|
||||
@Json(name = "ciphertext") val ciphertext: String? = null,
|
||||
@Json(name = "mac") val mac: String? = null,
|
||||
@Json(name = "ephemeral") val ephemeral: String? = null
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Facility method to convert from object which must be comprised of maps, lists,
|
||||
* strings, numbers, booleans and nulls.
|
||||
*/
|
||||
fun fromJson(obj: Any?): EncryptedSecretContent? {
|
||||
return MoshiProvider.providesMoshi()
|
||||
.adapter(EncryptedSecretContent::class.java)
|
||||
.fromJsonValue(obj)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 KeyInfoResult {
|
||||
data class Success(val keyInfo: KeyInfo) : KeyInfoResult()
|
||||
data class Error(val error: SharedSecretStorageError) : KeyInfoResult()
|
||||
|
||||
fun isSuccess(): Boolean = this is Success
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
interface KeySigner {
|
||||
fun sign(canonicalJson: String): Map<String, Map<String, 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.securestorage
|
||||
|
||||
data class SSSSKeyCreationInfo (
|
||||
val keyId: String = "",
|
||||
var content: SecretStorageKeyContent?,
|
||||
val recoveryKey: String = ""
|
||||
)
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.deriveKey
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||
|
||||
/** Tag class */
|
||||
interface SSSSKeySpec
|
||||
|
||||
data class Curve25519AesSha2KeySpec(
|
||||
val privateKey: ByteArray
|
||||
) : SSSSKeySpec {
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): Curve25519AesSha2KeySpec {
|
||||
return Curve25519AesSha2KeySpec(
|
||||
privateKey = deriveKey(
|
||||
passphrase,
|
||||
salt,
|
||||
iterations,
|
||||
progressListener
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun fromRecoveryKey(recoveryKey: String): Curve25519AesSha2KeySpec? {
|
||||
return extractCurveKeyFromRecoveryKey(recoveryKey)?.let {
|
||||
Curve25519AesSha2KeySpec(
|
||||
privateKey = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Curve25519AesSha2KeySpec
|
||||
|
||||
if (!privateKey.contentEquals(other.privateKey)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return privateKey.contentHashCode()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
|
||||
/**
|
||||
*
|
||||
* The contents of the account data for the key will include an algorithm property, which indicates the encryption algorithm used, as well as a name property,
|
||||
* which is a human-readable name.
|
||||
* The contents will be signed as signed JSON using the user's master cross-signing key. Other properties depend on the encryption algorithm.
|
||||
*
|
||||
*
|
||||
* "content": {
|
||||
* "algorithm": "m.secret_storage.v1.curve25519-aes-sha2",
|
||||
* "passphrase": {
|
||||
* "algorithm": "m.pbkdf2",
|
||||
* "iterations": 500000,
|
||||
* "salt": "IrswcMWnYieBALCAOMBw9k93xSzlc2su"
|
||||
* },
|
||||
* "pubkey": "qql1q3IvBbwMU97zLnyh9HYW5x/zqTy5eoK1n+9fm1Y",
|
||||
* "signatures": {
|
||||
* "@valere35:matrix.org": {
|
||||
* "ed25519:nOUQYiH9L8uKp5JajqiQyv+Loa3+lsdil7UBverz/Ko": "QtePmwfUL7+SHYRJT/HaTgF7gUFog1E/wtUCt0qc5aB8N+Sz5iCOvQ0KtaFHQ5SJzsBlYH8k7ejoBc0RcnU7BA"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
data class KeyInfo(
|
||||
val id: String,
|
||||
val content: SecretStorageKeyContent
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SecretStorageKeyContent(
|
||||
/** Currently support m.secret_storage.v1.curve25519-aes-sha2 */
|
||||
@Json(name = "algorithm") val algorithm: String? = null,
|
||||
@Json(name = "name") val name: String? = null,
|
||||
@Json(name = "passphrase") val passphrase: SSSSPassphrase? = null,
|
||||
@Json(name = "pubkey") val publicKey: String? = null,
|
||||
@Json(name = "signatures")
|
||||
var signatures: Map<String, Map<String, String>>? = null
|
||||
) {
|
||||
|
||||
private fun signalableJSONDictionary(): Map<String, Any> {
|
||||
val map = HashMap<String, Any>()
|
||||
algorithm?.let { map["algorithm"] = it }
|
||||
name?.let { map["name"] = it }
|
||||
publicKey?.let { map["pubkey"] = it }
|
||||
passphrase?.let { ssspp ->
|
||||
map["passphrase"] = mapOf(
|
||||
"algorithm" to ssspp.algorithm,
|
||||
"iterations" to ssspp.salt,
|
||||
"salt" to ssspp.salt
|
||||
)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
fun canonicalSignable(): String {
|
||||
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Facility method to convert from object which must be comprised of maps, lists,
|
||||
* strings, numbers, booleans and nulls.
|
||||
*/
|
||||
fun fromJson(obj: Any?): SecretStorageKeyContent? {
|
||||
return MoshiProvider.providesMoshi()
|
||||
.adapter(SecretStorageKeyContent::class.java)
|
||||
.fromJsonValue(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SSSSPassphrase(
|
||||
@Json(name = "algorithm") val algorithm: String?,
|
||||
@Json(name = "iterations") val iterations: Int,
|
||||
@Json(name = "salt") val salt: String?
|
||||
)
|
||||
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 SharedSecretStorageError(message: String?) : Throwable(message) {
|
||||
|
||||
data class UnknownSecret(val secretName: String) : SharedSecretStorageError("Unknown Secret $secretName")
|
||||
data class UnknownKey(val keyId: String) : SharedSecretStorageError("Unknown key $keyId")
|
||||
data class UnknownAlgorithm(val keyId: String) : SharedSecretStorageError("Unknown algorithm $keyId")
|
||||
data class UnsupportedAlgorithm(val algorithm: String) : SharedSecretStorageError("Unknown algorithm $algorithm")
|
||||
data class SecretNotEncrypted(val secretName: String) : SharedSecretStorageError("Missing content for secret $secretName")
|
||||
data class SecretNotEncryptedWithKey(val secretName: String, val keyId: String) : SharedSecretStorageError("Missing content for secret $secretName with key $keyId")
|
||||
object BadKeyFormat : SharedSecretStorageError("Bad Key Format")
|
||||
object ParsingError : SharedSecretStorageError("parsing Error")
|
||||
data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage)
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.api.session.securestorage
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
|
||||
/**
|
||||
* Some features may require clients to store encrypted data on the server so that it can be shared securely between clients.
|
||||
@ -39,7 +40,29 @@ interface SharedSecretStorageService {
|
||||
*
|
||||
* @return {string} the ID of the key
|
||||
*/
|
||||
fun addKey(algorithm: String, opts: Map<String, Any>, keyId: String, callback: MatrixCallback<String>)
|
||||
fun generateKey(keyId: String,
|
||||
keyName: String,
|
||||
keySigner: KeySigner,
|
||||
callback: MatrixCallback<SSSSKeyCreationInfo>)
|
||||
|
||||
fun generateKeyWithPassphrase(keyId: String,
|
||||
keyName: String,
|
||||
passphrase: String,
|
||||
keySigner: KeySigner,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<SSSSKeyCreationInfo>)
|
||||
|
||||
fun getKey(keyId: String): KeyInfoResult
|
||||
|
||||
/**
|
||||
* A key can be marked as the "default" key by setting the user's account_data with event type m.secret_storage.default_key
|
||||
* to an object that has the ID of the key as its key property.
|
||||
* The default key will be used to encrypt all secrets that the user would expect to be available on all their clients.
|
||||
* Unless the user specifies otherwise, clients will try to use the default key to decrypt secrets.
|
||||
*/
|
||||
fun getDefaultKey(): KeyInfoResult
|
||||
|
||||
fun setDefaultKey(keyId: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Check whether we have a key with a given ID.
|
||||
@ -51,23 +74,29 @@ interface SharedSecretStorageService {
|
||||
|
||||
/**
|
||||
* Store an encrypted secret on the server
|
||||
* Clients MUST ensure that the key is trusted before using it to encrypt secrets.
|
||||
*
|
||||
* @param name The name of the secret
|
||||
* @param secret The secret contents.
|
||||
* @param keys The IDs of the keys to use to encrypt the secret or null to use the default key.
|
||||
* @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>)
|
||||
|
||||
/**
|
||||
* Use this call to determine which SSSSKeySpec to use for requesting secret
|
||||
*/
|
||||
fun getAlgorithmsForSecret(name: String): List<KeyInfoResult>
|
||||
|
||||
/**
|
||||
* Get an encrypted secret from the shared storage
|
||||
*
|
||||
* @param name The name of the secret
|
||||
* @param keyId The id of the key that should be used to decrypt
|
||||
* @param keyId The id of the key that should be used to decrypt (null for default key)
|
||||
* @param privateKey the passphrase/secret
|
||||
*
|
||||
* @return The decrypted value
|
||||
*/
|
||||
fun getSecret(name: String, keyId: String, privateKey: String) : String
|
||||
@Throws
|
||||
|
||||
fun getSecret(name: String, keyId: String?, secretKey: SSSSKeySpec, callback: MatrixCallback<String>)
|
||||
}
|
||||
|
@ -40,8 +40,28 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersi
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.*
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.*
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.RoomKeysBackupData
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteBackupTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
|
@ -83,10 +83,10 @@ fun retrievePrivateKeyWithPassword(password: String,
|
||||
* @return a private key.
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun deriveKey(password: String,
|
||||
salt: String,
|
||||
iterations: Int,
|
||||
progressListener: ProgressListener?): ByteArray {
|
||||
fun deriveKey(password: String,
|
||||
salt: String,
|
||||
iterations: Int,
|
||||
progressListener: ProgressListener?): ByteArray {
|
||||
// Note: copied and adapted from MXMegolmExportEncryption
|
||||
val t0 = System.currentTimeMillis()
|
||||
|
||||
|
@ -0,0 +1,366 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.secrets
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
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.KeyInfo
|
||||
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.SSSSKeyCreationInfo
|
||||
import im.vector.matrix.android.api.session.securestorage.SSSSKeySpec
|
||||
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.SharedSecretStorageError
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
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.extensions.foldToCallback
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.olm.OlmPkDecryption
|
||||
import org.matrix.olm.OlmPkEncryption
|
||||
import org.matrix.olm.OlmPkMessage
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultSharedSecureStorage @Inject constructor(
|
||||
private val accountDataService: AccountDataService,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
) : SharedSecretStorageService {
|
||||
|
||||
override fun generateKey(keyId: String,
|
||||
keyName: String,
|
||||
keySigner: KeySigner,
|
||||
callback: MatrixCallback<SSSSKeyCreationInfo>) {
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
val pkDecryption = OlmPkDecryption()
|
||||
val pubKey: String
|
||||
val privateKey: ByteArray
|
||||
try {
|
||||
pubKey = pkDecryption.generateKey()
|
||||
privateKey = pkDecryption.privateKey()
|
||||
} catch (failure: Throwable) {
|
||||
return@launch Unit.also {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
} finally {
|
||||
pkDecryption.releaseDecryption()
|
||||
}
|
||||
|
||||
val storageKeyContent = SecretStorageKeyContent(
|
||||
name = keyName,
|
||||
algorithm = ALGORITHM_CURVE25519_AES_SHA2,
|
||||
passphrase = null,
|
||||
publicKey = pubKey
|
||||
)
|
||||
|
||||
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let {
|
||||
storageKeyContent.copy(
|
||||
signatures = it
|
||||
)
|
||||
} ?: storageKeyContent
|
||||
|
||||
accountDataService.updateAccountData(
|
||||
"$KEY_ID_BASE.$keyId",
|
||||
signedContent,
|
||||
object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
callback.onSuccess(SSSSKeyCreationInfo(
|
||||
keyId = keyId,
|
||||
content = storageKeyContent,
|
||||
recoveryKey = computeRecoveryKey(privateKey)
|
||||
))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun generateKeyWithPassphrase(keyId: String,
|
||||
keyName: String,
|
||||
passphrase: String,
|
||||
keySigner: KeySigner,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<SSSSKeyCreationInfo>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
|
||||
val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener)
|
||||
|
||||
val pkDecryption = OlmPkDecryption()
|
||||
val pubKey: String
|
||||
try {
|
||||
pubKey = pkDecryption.setPrivateKey(privatePart.privateKey)
|
||||
} catch (failure: Throwable) {
|
||||
return@launch Unit.also {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
} finally {
|
||||
pkDecryption.releaseDecryption()
|
||||
}
|
||||
|
||||
val storageKeyContent = SecretStorageKeyContent(
|
||||
algorithm = ALGORITHM_CURVE25519_AES_SHA2,
|
||||
passphrase = SSSSPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt),
|
||||
publicKey = pubKey
|
||||
)
|
||||
|
||||
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let {
|
||||
storageKeyContent.copy(
|
||||
signatures = it
|
||||
)
|
||||
} ?: storageKeyContent
|
||||
|
||||
accountDataService.updateAccountData(
|
||||
"$KEY_ID_BASE.$keyId",
|
||||
signedContent,
|
||||
object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
callback.onSuccess(SSSSKeyCreationInfo(
|
||||
keyId = keyId,
|
||||
content = storageKeyContent,
|
||||
recoveryKey = computeRecoveryKey(privatePart.privateKey)
|
||||
))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasKey(keyId: String): Boolean {
|
||||
return accountDataService.getAccountData("$KEY_ID_BASE.$keyId") != null
|
||||
}
|
||||
|
||||
override fun getKey(keyId: String): KeyInfoResult {
|
||||
val accountData = accountDataService.getAccountData("$KEY_ID_BASE.$keyId")
|
||||
?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(keyId))
|
||||
return SecretStorageKeyContent.fromJson(accountData.content)?.let {
|
||||
KeyInfoResult.Success(
|
||||
KeyInfo(id = keyId, content = it)
|
||||
)
|
||||
} ?: KeyInfoResult.Error(SharedSecretStorageError.UnknownAlgorithm(keyId))
|
||||
}
|
||||
|
||||
override fun setDefaultKey(keyId: String, callback: MatrixCallback<Unit>) {
|
||||
val existingKey = getKey(keyId)
|
||||
if (existingKey is KeyInfoResult.Success) {
|
||||
accountDataService.updateAccountData(DEFAULT_KEY_ID,
|
||||
mapOf("key" to keyId),
|
||||
callback
|
||||
)
|
||||
} else {
|
||||
callback.onFailure(SharedSecretStorageError.UnknownKey(keyId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDefaultKey(): KeyInfoResult {
|
||||
val accountData = accountDataService.getAccountData(DEFAULT_KEY_ID)
|
||||
?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID))
|
||||
val keyId = accountData.content["key"] as? String
|
||||
?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID))
|
||||
return getKey(keyId)
|
||||
}
|
||||
|
||||
override fun storeSecret(name: String, secretBase64: String, keys: List<String>?, callback: MatrixCallback<Unit>) {
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
val encryptedContents = HashMap<String, EncryptedSecretContent>()
|
||||
try {
|
||||
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
//use default key
|
||||
val key = getDefaultKey()
|
||||
when (key) {
|
||||
is KeyInfoResult.Success -> {
|
||||
if (key.keyInfo.content.algorithm == ALGORITHM_CURVE25519_AES_SHA2) {
|
||||
withOlmEncryption { olmEncrypt ->
|
||||
olmEncrypt.setRecipientKey(key.keyInfo.content.publicKey)
|
||||
val encryptedResult = olmEncrypt.encrypt(secretBase64)
|
||||
encryptedContents[key.keyInfo.id] = EncryptedSecretContent(
|
||||
ciphertext = encryptedResult.mCipherText,
|
||||
ephemeral = encryptedResult.mEphemeralKey,
|
||||
mac = encryptedResult.mMac
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Unknown algorithm
|
||||
callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: ""))
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
is KeyInfoResult.Error -> {
|
||||
callback.onFailure(key.error)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keys.forEach {
|
||||
val keyId = it
|
||||
// encrypt the content
|
||||
val key = getKey(keyId)
|
||||
when (key) {
|
||||
is KeyInfoResult.Success -> {
|
||||
if (key.keyInfo.content.algorithm == ALGORITHM_CURVE25519_AES_SHA2) {
|
||||
withOlmEncryption { olmEncrypt ->
|
||||
olmEncrypt.setRecipientKey(key.keyInfo.content.publicKey)
|
||||
val encryptedResult = 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(
|
||||
type = name,
|
||||
data = mapOf(
|
||||
"encrypted" to encryptedContents
|
||||
),
|
||||
callback = callback
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add default key
|
||||
}
|
||||
|
||||
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
|
||||
val accountData = accountDataService.getAccountData(name)
|
||||
?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.UnknownSecret(name)))
|
||||
val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *>
|
||||
?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.SecretNotEncrypted(name)))
|
||||
|
||||
val results = ArrayList<KeyInfoResult>()
|
||||
encryptedContent.keys.forEach {
|
||||
(it as? String)?.let { keyId ->
|
||||
results.add(getKey(keyId))
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
override fun getSecret(name: String, keyId: String?, secretKey: SSSSKeySpec, callback: MatrixCallback<String>) {
|
||||
val accountData = accountDataService.getAccountData(name) ?: return Unit.also {
|
||||
callback.onFailure(SharedSecretStorageError.UnknownSecret(name))
|
||||
}
|
||||
val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> ?: return Unit.also {
|
||||
callback.onFailure(SharedSecretStorageError.SecretNotEncrypted(name))
|
||||
}
|
||||
val key = keyId?.let { getKey(it) } as? KeyInfoResult.Success ?: getDefaultKey() as? KeyInfoResult.Success ?: return Unit.also {
|
||||
callback.onFailure(SharedSecretStorageError.UnknownKey(name))
|
||||
}
|
||||
|
||||
val encryptedForKey = encryptedContent[key.keyInfo.id] ?: return Unit.also {
|
||||
callback.onFailure(SharedSecretStorageError.SecretNotEncryptedWithKey(name, key.keyInfo.id))
|
||||
}
|
||||
|
||||
val secretContent = EncryptedSecretContent.fromJson(encryptedForKey)
|
||||
?: return Unit.also {
|
||||
callback.onFailure(SharedSecretStorageError.ParsingError)
|
||||
}
|
||||
|
||||
val algorithm = key.keyInfo.content
|
||||
if (ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) {
|
||||
val keySpec = secretKey as? Curve25519AesSha2KeySpec ?: return Unit.also {
|
||||
callback.onFailure(SharedSecretStorageError.BadKeyFormat)
|
||||
}
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
kotlin.runCatching {
|
||||
// decryt from recovery key
|
||||
val keyBytes = keySpec.privateKey
|
||||
val decryption = OlmPkDecryption()
|
||||
try {
|
||||
decryption.setPrivateKey(keyBytes)
|
||||
decryption.decrypt(OlmPkMessage().apply {
|
||||
mCipherText = secretContent.ciphertext
|
||||
mEphemeralKey = secretContent.ephemeral
|
||||
mMac = secretContent.mac
|
||||
})
|
||||
} catch (failure: Throwable) {
|
||||
throw failure
|
||||
} finally {
|
||||
decryption.releaseDecryption()
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
} else {
|
||||
callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: ""))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_ID_BASE = "m.secret_storage.key"
|
||||
const val ENCRYPTED = "encrypted"
|
||||
const val DEFAULT_KEY_ID = "m.secret_storage.default_key"
|
||||
|
||||
const val ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2"
|
||||
|
||||
fun withOlmEncryption(block: (OlmPkEncryption) -> Unit) {
|
||||
val olmPkEncryption = OlmPkEncryption()
|
||||
try {
|
||||
block(olmPkEncryption)
|
||||
} catch (failure: Throwable) {
|
||||
throw failure
|
||||
} finally {
|
||||
olmPkEncryption.releaseEncryption()
|
||||
}
|
||||
}
|
||||
|
||||
fun withOlmDecryption(block: (OlmPkDecryption) -> Unit) {
|
||||
val olmPkDecryption = OlmPkDecryption()
|
||||
try {
|
||||
block(olmPkDecryption)
|
||||
} catch (failure: Throwable) {
|
||||
throw failure
|
||||
} finally {
|
||||
olmPkDecryption.releaseDecryption()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.secrets
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
|
||||
internal class DefaultSharedSecureStorage : SharedSecretStorageService {
|
||||
|
||||
override fun addKey(algorithm: String, opts: Map<String, Any>, keyId: String, callback: MatrixCallback<String>) {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun hasKey(keyId: String): Boolean {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun storeSecret(name: String, secretBase64: String, keys: List<String>?, callback: MatrixCallback<Unit>) {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun getSecret(name: String, keyId: String, privateKey: String): String {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules
|
||||
|
||||
@ -31,7 +31,7 @@ object MoshiProvider {
|
||||
|
||||
private val moshi: Moshi = Moshi.Builder()
|
||||
.add(UriMoshiAdapter())
|
||||
.add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataFallback::class.java)
|
||||
.add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataEvent::class.java)
|
||||
.registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES)
|
||||
.registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST)
|
||||
.registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES)
|
||||
|
@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.pushers.PushersService
|
||||
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
import im.vector.matrix.android.api.session.signout.SignOutService
|
||||
import im.vector.matrix.android.api.session.sync.FilterService
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
@ -93,6 +94,7 @@ internal class DefaultSession @Inject constructor(
|
||||
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
|
||||
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
|
||||
private val accountDataService: Lazy<AccountDataService>,
|
||||
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
||||
private val shieldTrustUpdater: ShieldTrustUpdater)
|
||||
: Session,
|
||||
RoomService by roomService.get(),
|
||||
@ -111,6 +113,9 @@ internal class DefaultSession @Inject constructor(
|
||||
ProfileService by profileService.get(),
|
||||
AccountDataService by accountDataService.get() {
|
||||
|
||||
override val sharedSecretStorageService: SharedSecretStorageService
|
||||
get() = _sharedSecretStorageService.get()
|
||||
|
||||
private var isOpen = false
|
||||
|
||||
private var syncThread: SyncThread? = null
|
||||
|
@ -35,6 +35,8 @@ import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecureStorage
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver
|
||||
import im.vector.matrix.android.internal.database.LiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
|
||||
@ -268,4 +270,7 @@ internal abstract class SessionModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindAccountDataServiceService(accountDataService: DefaultAccountDataService): AccountDataService
|
||||
|
||||
@Binds
|
||||
abstract fun bindSharedSecuredSecretStorageService(service: DefaultSharedSecureStorage): SharedSecretStorageService
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UserAccountDataFallback(
|
||||
data class UserAccountDataEvent(
|
||||
@Json(name = "type") override val type: String,
|
||||
@Json(name = "content") val content: Map<String, Any>
|
||||
) : UserAccountData()
|
@ -28,8 +28,7 @@ import im.vector.matrix.android.internal.database.model.UserAccountDataEntity
|
||||
import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import javax.inject.Inject
|
||||
@ -44,17 +43,17 @@ internal class DefaultAccountDataService @Inject constructor(
|
||||
private val moshi = MoshiProvider.providesMoshi()
|
||||
private val adapter = moshi.adapter<Map<String, Any>>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
|
||||
override fun getAccountData(type: String): UserAccountData? {
|
||||
override fun getAccountData(type: String): UserAccountDataEvent? {
|
||||
return getAccountData(listOf(type)).firstOrNull()
|
||||
}
|
||||
|
||||
override fun getLiveAccountData(type: String): LiveData<Optional<UserAccountData>> {
|
||||
override fun getLiveAccountData(type: String): LiveData<Optional<UserAccountDataEvent>> {
|
||||
return Transformations.map(getLiveAccountData(listOf(type))) {
|
||||
it.firstOrNull()?.toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAccountData(filterType: List<String>): List<UserAccountData> {
|
||||
override fun getAccountData(filterType: List<String>): List<UserAccountDataEvent> {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where(UserAccountDataEntity::class.java)
|
||||
.apply {
|
||||
@ -64,7 +63,7 @@ internal class DefaultAccountDataService @Inject constructor(
|
||||
}
|
||||
}?.mapNotNull { entity ->
|
||||
entity.type?.let { type ->
|
||||
UserAccountDataFallback(
|
||||
UserAccountDataEvent(
|
||||
type = type,
|
||||
content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap()
|
||||
)
|
||||
@ -72,7 +71,7 @@ internal class DefaultAccountDataService @Inject constructor(
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
override fun getLiveAccountData(filterType: List<String>): LiveData<List<UserAccountData>> {
|
||||
override fun getLiveAccountData(filterType: List<String>): LiveData<List<UserAccountDataEvent>> {
|
||||
return monarchy.findAllMappedWithChanges({ realm ->
|
||||
realm.where(UserAccountDataEntity::class.java)
|
||||
.apply {
|
||||
@ -81,7 +80,7 @@ internal class DefaultAccountDataService @Inject constructor(
|
||||
}
|
||||
}
|
||||
}, { entity ->
|
||||
UserAccountDataFallback(
|
||||
UserAccountDataEvent(
|
||||
type = entity.type ?: "",
|
||||
content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap()
|
||||
)
|
||||
|
@ -22,7 +22,7 @@ import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.configureWith
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
@ -55,9 +55,9 @@ class AccountDataFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun didTap(data: UserAccountData) {
|
||||
val fb = data as? UserAccountDataFallback ?: return
|
||||
val fb = data as? UserAccountDataEvent ?: return
|
||||
val jsonString = MoshiProvider.providesMoshi()
|
||||
.adapter(UserAccountDataFallback::class.java)
|
||||
.adapter(UserAccountDataEvent::class.java)
|
||||
.toJson(fb)
|
||||
JsonViewerBottomSheetDialog.newInstance(jsonString)
|
||||
.show(childFragmentManager, "JSON_VIEWER")
|
||||
|
Loading…
x
Reference in New Issue
Block a user