Implement AES backup algorithm (WIP need to check with valere the private key thing)

This commit is contained in:
ganfra 2022-08-17 19:43:01 +02:00 committed by Valere
parent 1d26207df7
commit 03272a9c8f
7 changed files with 174 additions and 82 deletions

View File

@ -42,6 +42,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCurve25519AuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.getRoom
@ -321,7 +322,8 @@ class KeysBackupTest : InstrumentedTest {
put(cryptoTestData.roomId, roomKeysBackupData)
}
)
val sessionsData = algorithm.decryptSessions(keyBackupCreationInfo.recoveryKey, keysBackupData)
algorithm.setRecoveryKey(keyBackupCreationInfo.recoveryKey)
val sessionsData = algorithm.decryptSessions(keysBackupData)
val sessionData = sessionsData.firstOrNull()
assertNotNull(sessionData)
// - Compare the decrypted megolm key with the original one

View File

@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.listeners.ProgressListener
@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.util.JsonDict
@ -630,8 +632,9 @@ internal class DefaultKeysBackupService @Inject constructor(
// Get backed up keys from the homeserver
val data = getKeys(sessionId, roomId, keysVersionResult.version)
algorithm?.setRecoveryKey(recoveryKey)
val sessionsData = withContext(coroutineDispatchers.computation) {
algorithm?.decryptSessions(recoveryKey, data)
algorithm?.decryptSessions(data)
}.orEmpty()
// Do not trigger a backup for them if they come from the backup version we are using
val backUp = keysVersionResult.version != keysBackupVersion?.version
@ -998,7 +1001,8 @@ internal class DefaultKeysBackupService @Inject constructor(
private fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: String, keysBackupData: KeysVersionResult): Boolean {
return try {
val algorithm = algorithmFactory.create(keysBackupData)
val isValid = algorithm.keyMatches(recoveryKey)
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) ?: return false
val isValid = algorithm.keyMatches(privateKey)
algorithm.release()
isValid
} catch (failure: Throwable) {
@ -1133,7 +1137,8 @@ internal class DefaultKeysBackupService @Inject constructor(
// Gather data to send to the homeserver
// roomId -> sessionId -> MXKeyBackupData
val keysBackupData = KeysBackupData()
val recoveryKey = cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
algorithm?.setRecoveryKey(recoveryKey)
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
val olmInboundGroupSession = olmInboundGroupSessionWrapper.session
@ -1242,7 +1247,9 @@ internal class DefaultKeysBackupService @Inject constructor(
}
?: return null
val sessionBackupData = algorithm?.encryptSession(sessionData) ?: return null
val sessionBackupData = tryOrNull {
algorithm?.encryptSession(sessionData)
} ?: return null
return KeyBackupData(
firstMessageIndex = try {
olmInboundGroupSessionWrapper.session.firstKnownIndex
@ -1265,6 +1272,10 @@ internal class DefaultKeysBackupService @Inject constructor(
return sessionData.sharedHistory
}
private fun getPrivateKey(): ByteArray {
return byteArrayOf()
}
/* ==========================================================================================
* For test only
* ========================================================================================== */

View File

@ -20,15 +20,24 @@ import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_AES_256_BACKUP
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAes256AuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
import org.matrix.android.sdk.internal.crypto.tools.AesHmacSha2
import org.matrix.olm.OlmException
import timber.log.Timber
import java.security.InvalidParameterException
import java.util.Arrays
internal class KeysBackupAes256Algorithm(keysVersions: KeysVersionResult) : KeysBackupAlgorithm {
override val untrusted: Boolean = true
override val untrusted: Boolean = false
private val aesAuthData: MegolmBackupAes256AuthData
private var privateKey: ByteArray? = null
init {
if (keysVersions.algorithm != MXCRYPTO_ALGORITHM_AES_256_BACKUP) {
@ -37,21 +46,92 @@ internal class KeysBackupAes256Algorithm(keysVersions: KeysVersionResult) : Keys
aesAuthData = keysVersions.getAuthDataAsMegolmBackupAuthData() as MegolmBackupAes256AuthData
}
override fun encryptSession(sessionData: MegolmSessionData): JsonDict? {
TODO("Not yet implemented")
override fun setRecoveryKey(recoveryKey: String?) {
privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
}
override fun decryptSessions(recoveryKey: String, data: KeysBackupData): List<MegolmSessionData> {
TODO("Not yet implemented")
override fun encryptSession(sessionData: MegolmSessionData): JsonDict? {
val privateKey = privateKey
if (privateKey == null || !keyMatches(privateKey)) {
Timber.e("Key does not match")
throw IllegalStateException("Key does not match")
}
val encryptedSessionBackupData = try {
val sessionDataJson = sessionData.asBackupJson()
AesHmacSha2.encrypt(privateKey = privateKey, secretName = sessionData.sessionId ?: "", sessionDataJson)
} catch (e: OlmException) {
Timber.e(e, "Error while encrypting backup data.")
null
} ?: return null
return mapOf(
"ciphertext" to encryptedSessionBackupData.cipherRawBytes.toBase64NoPadding(),
"mac" to encryptedSessionBackupData.mac.toBase64NoPadding(),
"iv" to encryptedSessionBackupData.initializationVector.toBase64NoPadding()
)
}
override fun decryptSessions(data: KeysBackupData): List<MegolmSessionData> {
val privateKey = privateKey
if (privateKey == null || !keyMatches(privateKey)) {
Timber.e("Invalid recovery key for this keys version")
throw InvalidParameterException("Invalid recovery key")
}
val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data
var sessionsFromHsCount = 0
for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) {
for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) {
sessionsFromHsCount++
val sessionData = decryptSession(keyBackupData.sessionData, sessionIdLoop, roomIdLoop, privateKey)
sessionData?.let {
sessionsData.add(it)
}
}
}
Timber.v(
"Decrypted ${sessionsData.size} keys out of $sessionsFromHsCount from the backup store on the homeserver"
)
return sessionsData
}
private fun decryptSession(sessionData: JsonDict, sessionId: String, roomId: String, privateKey: ByteArray): MegolmSessionData? {
val cipherRawBytes = sessionData["ciphertext"]?.toString()?.fromBase64() ?: return null
val mac = sessionData["mac"]?.toString()?.fromBase64() ?: throw IllegalStateException("Bad mac")
val iv = sessionData["iv"]?.toString()?.fromBase64() ?: ByteArray(16)
val encryptionInfo = AesHmacSha2.EncryptionInfo(
cipherRawBytes = cipherRawBytes,
mac = mac,
initializationVector = iv
)
return try {
val decrypted = AesHmacSha2.decrypt(privateKey, sessionId, encryptionInfo)
createMegolmSessionData(decrypted, sessionId, roomId)
} catch (e: Exception) {
Timber.e(e, "Exception while decrypting")
null
}
}
override fun release() {
TODO("Not yet implemented")
privateKey?.apply {
Arrays.fill(this, 0)
}
privateKey = null
}
override val authData: MegolmBackupAuthData = aesAuthData
override fun keyMatches(key: String): Boolean {
TODO("Not yet implemented")
override fun keyMatches(privateKey: ByteArray): Boolean {
return if (aesAuthData.mac != null) {
val keyCheckMac = AesHmacSha2.calculateKeyCheck(privateKey, aesAuthData.iv).mac
val authDataMac = aesAuthData.mac.fromBase64()
return keyCheckMac.contentEquals(authDataMac)
} else {
// if we have no information, we have to assume the key is right
true
}
}
}

View File

@ -20,13 +20,43 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
import org.matrix.android.sdk.internal.di.MoshiProvider
internal interface KeysBackupAlgorithm {
val authData: MegolmBackupAuthData
val untrusted: Boolean
fun setRecoveryKey(recoveryKey: String?)
fun encryptSession(sessionData: MegolmSessionData): JsonDict?
fun decryptSessions(recoveryKey: String, data: KeysBackupData): List<MegolmSessionData>
fun keyMatches(key: String): Boolean
fun decryptSessions(data: KeysBackupData): List<MegolmSessionData>
fun keyMatches(privateKey: ByteArray): Boolean
fun release()
}
internal fun KeysBackupAlgorithm.createMegolmSessionData(decrypted: String, sessionId: String, roomId: String): MegolmSessionData? {
val moshi = MoshiProvider.providesMoshi()
val adapter = moshi.adapter(MegolmSessionData::class.java)
val sessionBackupData: MegolmSessionData = adapter.fromJson(decrypted) ?: return null
return sessionBackupData.copy(
sessionId = sessionId,
roomId = roomId,
untrusted = untrusted
)
}
internal fun MegolmSessionData.asBackupJson(): String {
val sessionBackupData = mapOf(
"algorithm" to algorithm,
"sender_key" to senderKey,
"sender_claimed_keys" to senderClaimedKeys,
"forwarding_curve25519_key_chain" to (forwardingCurve25519KeyChain.orEmpty()),
"session_key" to sessionKey,
"org.matrix.msc3061.shared_history" to sharedHistory,
"untrusted" to untrusted
)
return MoshiProvider.providesMoshi()
.adapter(Map::class.java)
.toJson(sessionBackupData)
}

View File

@ -24,7 +24,6 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromR
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.olm.OlmException
import org.matrix.olm.OlmPkDecryption
import org.matrix.olm.OlmPkEncryption
@ -40,6 +39,7 @@ internal class KeysBackupCurve25519Algorithm(keysVersions: KeysVersionResult) :
private val publicKey: String
private val encryptionKey: OlmPkEncryption
private var privateKey: ByteArray? = null
init {
if (keysVersions.algorithm != MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP) {
@ -52,26 +52,17 @@ internal class KeysBackupCurve25519Algorithm(keysVersions: KeysVersionResult) :
}
}
override fun setRecoveryKey(recoveryKey: String?) {
privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
}
override fun encryptSession(sessionData: MegolmSessionData): JsonDict? {
val sessionBackupData = mapOf(
"algorithm" to sessionData.algorithm,
"sender_key" to sessionData.senderKey,
"sender_claimed_keys" to sessionData.senderClaimedKeys,
"forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()),
"session_key" to sessionData.sessionKey,
"org.matrix.msc3061.shared_history" to sessionData.sharedHistory,
"untrusted" to sessionData.untrusted
)
val json = MoshiProvider.providesMoshi()
.adapter(Map::class.java)
.toJson(sessionBackupData)
val encryptedSessionBackupData = try {
encryptionKey.encrypt(json)
val sessionDataJson = sessionData.asBackupJson()
encryptionKey.encrypt(sessionDataJson)
} catch (e: OlmException) {
Timber.e(e, "Error while encrypting backup data.")
null
throw e
} ?: return null
return mapOf(
@ -81,29 +72,25 @@ internal class KeysBackupCurve25519Algorithm(keysVersions: KeysVersionResult) :
)
}
override fun decryptSessions(recoveryKey: String, data: KeysBackupData): List<MegolmSessionData> {
fun pkDecryptionFromRecoveryKey(recoveryKey: String): OlmPkDecryption? {
// Extract the primary key
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
// Built the PK decryption with it
override fun decryptSessions(data: KeysBackupData): List<MegolmSessionData> {
fun pkDecryptionFromPrivateKey(privateKey: ByteArray): OlmPkDecryption? {
var decryption: OlmPkDecryption? = null
if (privateKey != null) {
try {
decryption = OlmPkDecryption()
decryption.setPrivateKey(privateKey)
} catch (e: OlmException) {
Timber.e(e, "OlmException")
}
}
return decryption
}
if (!keyMatches(recoveryKey)) {
val privateKey = this.privateKey
if (privateKey == null || !keyMatches(privateKey)) {
Timber.e("Invalid recovery key for this keys version")
throw InvalidParameterException("Invalid recovery key")
}
// Get a PK decryption instance
val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
val decryption = pkDecryptionFromPrivateKey(privateKey)
if (decryption == null) {
// This should not happen anymore
Timber.e("Invalid recovery key. Error")
@ -121,6 +108,7 @@ internal class KeysBackupCurve25519Algorithm(keysVersions: KeysVersionResult) :
}
}
}
decryption.releaseDecryption()
Timber.v(
"Decrypted ${sessionsData.size} keys out of $sessionsFromHsCount from the backup store on the homeserver"
)
@ -128,55 +116,36 @@ internal class KeysBackupCurve25519Algorithm(keysVersions: KeysVersionResult) :
}
private fun decryptSession(sessionData: JsonDict, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {
var sessionBackupData: MegolmSessionData? = null
val ciphertext = sessionData["ciphertext"]?.toString()
val mac = sessionData["mac"]?.toString()
val ephemeralKey = sessionData["ephemeral"]?.toString()
if (ciphertext != null && mac != null && ephemeralKey != null) {
return if (ciphertext != null && mac != null && ephemeralKey != null) {
val encrypted = OlmPkMessage()
encrypted.mCipherText = ciphertext
encrypted.mMac = mac
encrypted.mEphemeralKey = ephemeralKey
try {
val decrypted = decryption.decrypt(encrypted)
val moshi = MoshiProvider.providesMoshi()
val adapter = moshi.adapter(MegolmSessionData::class.java)
sessionBackupData = adapter.fromJson(decrypted)
createMegolmSessionData(decrypted, sessionId, roomId)
} catch (e: OlmException) {
Timber.e(e, "OlmException")
Timber.e(e, "Exception while decrypting")
null
}
if (sessionBackupData != null) {
sessionBackupData = sessionBackupData.copy(
sessionId = sessionId,
roomId = roomId,
untrusted = untrusted
)
} else {
null
}
}
return sessionBackupData
}
override fun release() {
encryptionKey.releaseEncryption()
}
override val authData: MegolmBackupAuthData = curveAuthData
override fun keyMatches(key: String): Boolean {
fun pkPublicKeyFromRecoveryKey(recoveryKey: String): String? {
// Extract the primary key
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
if (privateKey == null) {
Timber.w("pkPublicKeyFromRecoveryKey: private key is null")
return null
}
override fun keyMatches(privateKey: ByteArray): Boolean {
fun pkPublicKeyFromPrivateKey(privateKey: ByteArray): String? {
// Built the PK decryption with it
val decryption = OlmPkDecryption()
val pkPublicKey = try {
@ -189,7 +158,7 @@ internal class KeysBackupCurve25519Algorithm(keysVersions: KeysVersionResult) :
return pkPublicKey
}
val publicKey = pkPublicKeyFromRecoveryKey(key)
val publicKey = pkPublicKeyFromPrivateKey(privateKey)
if (publicKey == null) {
Timber.w("Public key is null")
return false

View File

@ -221,12 +221,12 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val iv = cipherContent.initializationVector?.fromBase64() ?: ByteArray(16)
val cipher = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
val mac = cipherContent.mac?.fromBase64() ?: throw SharedSecretStorageError.BadMac
val encryptedResult = AesHmacSha2.Result(
val encryptionInfo = AesHmacSha2.EncryptionInfo(
cipherRawBytes = cipher,
initializationVector = iv,
mac = mac
)
return AesHmacSha2.decrypt(secretKey.privateKey, secretName, encryptedResult)
return AesHmacSha2.decrypt(secretKey.privateKey, secretName, encryptionInfo)
}
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {

View File

@ -27,7 +27,7 @@ import kotlin.experimental.and
internal object AesHmacSha2 {
class Result(
class EncryptionInfo(
val cipherRawBytes: ByteArray,
val mac: ByteArray,
val initializationVector: ByteArray
@ -55,7 +55,7 @@ internal object AesHmacSha2 {
* (We use AES-CTR to match file encryption and key exports.)
*/
@Throws
fun encrypt(privateKey: ByteArray, secretName: String, clearDataBase64: String, ivString: String? = null): Result {
fun encrypt(privateKey: ByteArray, secretName: String, clearDataBase64: String, ivString: String? = null): EncryptionInfo {
val pseudoRandomKey = HkdfSha256.deriveSecret(
privateKey,
ByteArray(32) { 0.toByte() },
@ -94,14 +94,14 @@ internal object AesHmacSha2 {
mac.init(macKeySpec)
val digest = mac.doFinal(cipherBytes)
return Result(
return EncryptionInfo(
cipherRawBytes = cipherBytes,
initializationVector = iv,
mac = digest
)
}
fun decrypt(privateKey: ByteArray, secretName: String, aesHmacSha2Result: Result): String {
fun decrypt(privateKey: ByteArray, secretName: String, aesHmacSha2Result: EncryptionInfo): String {
val pseudoRandomKey = HkdfSha256.deriveSecret(
privateKey,
zeroByteArray(32),
@ -140,9 +140,9 @@ internal object AesHmacSha2 {
* @param {ByteArray} [key] the key to use
* @param {string} [iv] The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @return [Result] An object that contains, `mac` and `iv` properties.
* @return [EncryptionInfo] An object that contains, `mac` and `iv` properties.
*/
fun calculateKeyCheck(key: ByteArray, iv: String?): Result {
fun calculateKeyCheck(key: ByteArray, iv: String?): EncryptionInfo {
val zerosStr = String(zeroByteArray(32))
return encrypt(key, "", zerosStr, iv)
}