Merge branch 'develop' into feature/Perf

This commit is contained in:
ganfra
2019-07-02 17:00:09 +02:00
69 changed files with 1251 additions and 248 deletions

View File

@ -5,7 +5,7 @@
* 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
* 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,
@ -14,21 +14,14 @@
* limitations under the License.
*/
package im.vector.riotredesign.core.glide;
package im.vector.riotredesign.core.glide
import android.content.Context;
import android.util.Log;
import com.bumptech.glide.load.Option
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
const val ElementToDecryptOptionKey = "im.vector.riotx.core.glide.ElementToDecrypt"
@GlideModule
public final class MyAppGlideModule extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setLogLevel(Log.ERROR);
}
val ELEMENT_TO_DECRYPT = Option.memory(
ElementToDecryptOptionKey, ElementToDecrypt("", "", ""))
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2019 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.riotredesign.core.glide
import android.content.Context
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule
import im.vector.riotredesign.core.extensions.vectorComponent
import im.vector.riotredesign.features.media.ImageContentRenderer
import java.io.InputStream
@GlideModule
class MyAppGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setLogLevel(Log.ERROR)
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry.append(ImageContentRenderer.Data::class.java,
InputStream::class.java,
VectorGlideModelLoaderFactory(context.vectorComponent().activeSessionHolder()))
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright 2019 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.riotredesign.core.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.features.media.ImageContentRenderer
import okhttp3.OkHttpClient
import okhttp3.Request
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import com.bumptech.glide.load.engine.Resource as Resource1
class VectorGlideModelLoaderFactory(private val activeSessionHolder: ActiveSessionHolder)
: ModelLoaderFactory<ImageContentRenderer.Data, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ImageContentRenderer.Data, InputStream> {
return VectorGlideModelLoader(activeSessionHolder)
}
override fun teardown() {
// Is there something to do here?
}
}
class VectorGlideModelLoader(private val activeSessionHolder: ActiveSessionHolder)
: ModelLoader<ImageContentRenderer.Data, InputStream> {
override fun handles(model: ImageContentRenderer.Data): Boolean {
// Always handle
return true
}
override fun buildLoadData(model: ImageContentRenderer.Data, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
return ModelLoader.LoadData(ObjectKey(model), VectorGlideDataFetcher(activeSessionHolder, model, width, height))
}
}
class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolder,
private val data: ImageContentRenderer.Data,
private val width: Int,
private val height: Int)
: DataFetcher<InputStream> {
val client = OkHttpClient()
override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
private var stream: InputStream? = null
override fun cleanup() {
cancel()
}
override fun getDataSource(): DataSource {
// ?
return DataSource.REMOTE
}
override fun cancel() {
if (stream != null) {
try {
stream?.close() // interrupts decode if any
stream = null
} catch (ignore: IOException) {
Timber.e(ignore)
}
}
}
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
Timber.v("Load data: $data")
if (data.isLocalFile()) {
val initialFile = File(data.url)
callback.onDataReady(FileInputStream(initialFile))
return
}
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val url = contentUrlResolver.resolveFullSize(data.url)
?: return
val request = Request.Builder()
.url(url)
.build()
val response = client.newCall(request).execute()
val inputStream = response.body()?.byteStream()
Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}")
if (!response.isSuccessful) {
callback.onLoadFailed(IOException("Unexpected code $response"))
return
}
stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) {
MXEncryptedAttachments.decryptAttachment(inputStream, data.elementToDecrypt)
} else {
inputStream
}
callback.onDataReady(stream)
}
}

View File

@ -21,9 +21,9 @@ import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
fun getFilenameFromUri(context: Context, uri: Uri): String? {
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
if (context != null && uri.scheme == "content") {
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst()) {

View File

@ -1,592 +0,0 @@
/*
* Copyright 2019 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.riotredesign.core.utils
import android.content.Context
import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
import java.io.*
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.SecureRandom
import java.util.*
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import javax.security.auth.x500.X500Principal
/**
* Offers simple methods to securely store secrets in an Android Application.
* The encryption keys are randomly generated and securely managed by the key store, thus your secrets
* are safe. You only need to remember a key alias to perform encrypt/decrypt operations.
*
* <b>Android M++</b>
* On android M+, the keystore can generates and store AES keys via API. But below API M this functionality
* is not available.
*
* <b>Android [K-M[</b>
* For android >=KITKAT and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a
* random secret key in generated to perform encryption.
* This secret key is encrypted with the public RSA key and stored with the encrypted secret.
* In order to decrypt the encrypted secret key will be retrieved then decrypted with the RSA private key.
*
* <b>Older androids</b>
* For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt.
* The salt and iv are stored with encrypted data.
*
* Sample usage:
* <code>
* val secret = "The answer is 42"
* val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context)
* //This can be stored anywhere e.g. encoded in b64 and stored in preference for example
*
* //to get back the secret, just call
* val kDecripted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context)
* </code>
*
* You can also just use this utility to store a secret key, and use any encryption algorthim that you want.
*
* Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
* add a pin or change the schema); So you might and with a useless pile of bytes.
*/
object SecretStoringUtils {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val AES_MODE = "AES/GCM/NoPadding";
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
const val FORMAT_API_M: Byte = 0
const val FORMAT_1: Byte = 1
const val FORMAT_2: Byte = 2
val keyStore: KeyStore by lazy {
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
load(null)
}
}
private val secureRandom = SecureRandom()
/**
* Encrypt the given secret using the android Keystore.
* On android >= M, will directly use the keystore to generate a symetric key
* On KitKat >= KitKat and <M, as symetric key gen is not available, will use an asymetric key generated
* in the keystore to encrypted a random symetric key. The encrypted symetric key is returned
* in the bytearray (in can be stored anywhere, it is encrypted)
* On older version a key in generated from alias with random salt.
*
* The secret is encrypted using the following method: AES/GCM/NoPadding
*/
@Throws(Exception::class)
fun securelyStoreString(secret: String, keyAlias: String, context: Context): ByteArray? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return encryptStringM(secret, keyAlias)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return encryptStringJ(secret, keyAlias, context)
} else {
return encryptForOldDevicesNotGood(secret, keyAlias)
}
}
/**
* Decrypt a secret that was encrypted by #securelyStoreString()
*/
@Throws(Exception::class)
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return decryptStringM(encrypted, keyAlias)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return decryptStringJ(encrypted, keyAlias, context)
} else {
return decryptForOldDevicesNotGood(encrypted, keyAlias)
}
}
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
saveSecureObjectM(keyAlias, output, any)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return saveSecureObjectK(keyAlias, output, any, context)
} else {
return saveSecureObjectOldNotGood(keyAlias, output, any)
}
}
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return loadSecureObjectM(keyAlias, inputStream)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return loadSecureObjectK(keyAlias, inputStream, context)
} else {
return loadSecureObjectOldNotGood(keyAlias, inputStream)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun getOrGenerateSymmetricKeyForAlias(alias: String): SecretKey {
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
?.secretKey
if (secretKeyEntry == null) {
//we generate it
val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val keyGenSpec = KeyGenParameterSpec.Builder(alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(128)
.build()
generator.init(keyGenSpec)
return generator.generateKey()
}
return secretKeyEntry
}
/*
Symetric Key Generation is only available in M, so before M the idea is to:
- Generate a pair of RSA keys;
- Generate a random AES key;
- Encrypt the AES key using the RSA public key;
- Store the encrypted AES
Generate a key pair for encryption
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry {
val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry)
if (privateKeyEntry != null) return privateKeyEntry
val start = Calendar.getInstance()
val end = Calendar.getInstance()
end.add(Calendar.YEAR, 30)
val spec = KeyPairGeneratorSpec.Builder(context)
.setAlias(alias)
.setSubject(X500Principal("CN=$alias"))
.setSerialNumber(BigInteger.TEN)
//.setEncryptionRequired() requires that the phone as a pin/schema
.setStartDate(start.time)
.setEndDate(end.time)
.build()
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE).run {
initialize(spec)
generateKeyPair()
}
return (keyStore.getEntry(alias, null) as KeyStore.PrivateKeyEntry)
}
@RequiresApi(Build.VERSION_CODES.M)
fun encryptStringM(text: String, keyAlias: String): ByteArray? {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
//we happen the iv to the final result
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
return formatMMake(iv, encryptedBytes)
}
@RequiresApi(Build.VERSION_CODES.M)
fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk))
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
}
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun encryptStringJ(text: String, keyAlias: String, context: Context): ByteArray? {
//we generate a random symetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
val sKey = SecretKeySpec(key, "AES")
//we encrypt this key thanks to the key store
val encryptedKey = rsaEncrypt(keyAlias, key, context)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
val iv = cipher.iv
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
return format1Make(encryptedKey, iv, encryptedBytes)
}
fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
val salt = ByteArray(8)
secureRandom.nextBytes(salt)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
val tmp = factory.generateSecret(spec)
val sKey = SecretKeySpec(tmp.encoded, "AES")
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
val iv = cipher.iv
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
return format2Make(salt, iv, encryptedBytes)
}
fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {
val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data))
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
val tmp = factory.generateSecret(spec)
val sKey = SecretKeySpec(tmp.encoded, "AES")
val cipher = Cipher.getInstance(AES_MODE)
// cipher.init(Cipher.ENCRYPT_MODE, sKey)
// val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
val specIV = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, sKey, specIV)
return String(cipher.doFinal(encrypted), Charsets.UTF_8)
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
fun decryptStringJ(data: ByteArray, keyAlias: String, context: Context): String? {
val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data))
//we need to decrypt the key
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
return String(cipher.doFinal(encrypted), Charsets.UTF_8)
}
@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
val iv = cipher.iv
val bos1 = ByteArrayOutputStream()
ObjectOutputStream(bos1).use {
it.writeObject(writeObject)
}
//Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV
val doFinal = cipher.doFinal(bos1.toByteArray())
output.write(FORMAT_API_M.toInt())
output.write(iv.size)
output.write(iv)
output.write(doFinal)
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) {
//we generate a random symetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
val sKey = SecretKeySpec(key, "AES")
//we encrypt this key thanks to the key store
val encryptedKey = rsaEncrypt(keyAlias, key, context)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
val iv = cipher.iv
val bos1 = ByteArrayOutputStream()
val cos = CipherOutputStream(bos1, cipher)
ObjectOutputStream(cos).use {
it.writeObject(writeObject)
}
output.write(FORMAT_1.toInt())
output.write((encryptedKey.size and 0xFF00).shr(8))
output.write(encryptedKey.size and 0x00FF)
output.write(encryptedKey)
output.write(iv.size)
output.write(iv)
output.write(bos1.toByteArray())
}
fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
val salt = ByteArray(8)
secureRandom.nextBytes(salt)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128))
val secretKey = SecretKeySpec(tmp.encoded, "AES")
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val bos1 = ByteArrayOutputStream()
ObjectOutputStream(bos1).use {
it.writeObject(writeObject)
}
//Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV
val doFinal = cipher.doFinal(bos1.toByteArray())
output.write(FORMAT_2.toInt())
output.write(salt.size)
output.write(salt)
output.write(iv.size)
output.write(iv)
output.write(doFinal)
}
// @RequiresApi(Build.VERSION_CODES.M)
// @Throws(IOException::class)
// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) {
// FileOutputStream(file).use {
// saveSecureObjectM(keyAlias, it, writeObject)
// }
// }
//
// @RequiresApi(Build.VERSION_CODES.M)
// @Throws(IOException::class)
// fun <T> loadSecureObjectM(keyAlias: String, file: File): T? {
// FileInputStream(file).use {
// return loadSecureObjectM<T>(keyAlias, it)
// }
// }
@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
val format = inputStream.read()
assert(format.toByte() == FORMAT_API_M)
val ivSize = inputStream.read()
val iv = ByteArray(ivSize)
inputStream.read(iv, 0, ivSize)
val cipher = Cipher.getInstance(AES_MODE)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
CipherInputStream(inputStream, cipher).use { cipherInputStream ->
ObjectInputStream(cipherInputStream).use {
val readObject = it.readObject()
return readObject as? T
}
}
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
@Throws(IOException::class)
fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? {
val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
//we need to decrypt the key
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
val encIS = ByteArrayInputStream(encrypted)
CipherInputStream(encIS, cipher).use { cipherInputStream ->
ObjectInputStream(cipherInputStream).use {
val readObject = it.readObject()
return readObject as? T
}
}
}
@Throws(Exception::class)
fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {
val (salt, iv, encrypted) = format2Extract(inputStream)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128))
val sKey = SecretKeySpec(tmp.encoded, "AES")
//we need to decrypt the key
val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, sKey, spec)
val encIS = ByteArrayInputStream(encrypted)
CipherInputStream(encIS, cipher).use {
ObjectInputStream(it).use {
val readObject = it.readObject()
return readObject as? T
}
}
}
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@Throws(Exception::class)
private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
// Encrypt the text
val inputCipher = Cipher.getInstance(RSA_MODE)
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
val outputStream = ByteArrayOutputStream()
val cipherOutputStream = CipherOutputStream(outputStream, inputCipher)
cipherOutputStream.write(secret)
cipherOutputStream.close()
return outputStream.toByteArray()
}
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@Throws(Exception::class)
private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
val output = Cipher.getInstance(RSA_MODE)
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)
val bos = ByteArrayOutputStream()
CipherInputStream(encrypted, output).use {
it.copyTo(bos)
}
return bos.toByteArray()
}
private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
val format = bis.read().toByte()
assert(format == FORMAT_API_M)
val ivSize = bis.read()
val iv = ByteArray(ivSize)
bis.read(iv, 0, ivSize)
val bos = ByteArrayOutputStream()
var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Pair(iv, encrypted)
}
private fun formatMMake(iv: ByteArray, data: ByteArray): ByteArray {
val bos = ByteArrayOutputStream(2 + iv.size + data.size)
bos.write(FORMAT_API_M.toInt())
bos.write(iv.size)
bos.write(iv)
bos.write(data)
return bos.toByteArray()
}
private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
val format = bis.read()
assert(format.toByte() == FORMAT_1)
val keySizeBig = bis.read()
val keySizeLow = bis.read()
val encryptedKeySize = keySizeBig.shl(8) + keySizeLow
val encryptedKey = ByteArray(encryptedKeySize)
bis.read(encryptedKey)
val ivSize = bis.read()
val iv = ByteArray(ivSize)
bis.read(iv)
val bos = ByteArrayOutputStream()
var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Triple(encryptedKey, iv, encrypted)
}
private fun format1Make(encryptedKey: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
val bos = ByteArrayOutputStream(4 + encryptedKey.size + iv.size + encryptedBytes.size)
bos.write(FORMAT_1.toInt())
bos.write((encryptedKey.size and 0xFF00).shr(8))
bos.write(encryptedKey.size and 0x00FF)
bos.write(encryptedKey)
bos.write(iv.size)
bos.write(iv)
bos.write(encryptedBytes)
return bos.toByteArray()
}
private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size)
bos.write(FORMAT_2.toInt())
bos.write(salt.size)
bos.write(salt)
bos.write(iv.size)
bos.write(iv)
bos.write(encryptedBytes)
return bos.toByteArray()
}
private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
val format = bis.read()
assert(format.toByte() == FORMAT_2)
val saltSize = bis.read()
val salt = ByteArray(saltSize)
bis.read(salt)
val ivSize = bis.read()
val iv = ByteArray(ivSize)
bis.read(iv)
val bos = ByteArrayOutputStream()
var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Triple(salt, iv, encrypted)
}
}

View File

@ -616,11 +616,11 @@ class RoomDetailFragment :
}
override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
vectorBaseActivity.notImplemented()
vectorBaseActivity.notImplemented("open file")
}
override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) {
vectorBaseActivity.notImplemented()
vectorBaseActivity.notImplemented("open audio file")
}
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {

View File

@ -16,6 +16,7 @@
package im.vector.riotredesign.features.home.room.detail
import android.net.Uri
import android.text.TextUtils
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@ -36,6 +37,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.rx.rx
import im.vector.riotredesign.R
import im.vector.riotredesign.core.intent.getFilenameFromUri
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.resources.UserPreferencesProvider
import im.vector.riotredesign.core.utils.LiveEvent
@ -360,13 +362,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachments = action.mediaFiles.map {
val nameWithExtension = getFilenameFromUri(null, Uri.parse(it.path))
ContentAttachmentData(
size = it.size,
duration = it.duration,
date = it.date,
height = it.height,
width = it.width,
name = it.name,
name = nameWithExtension ?: it.name,
path = it.path,
mimeType = it.mimeType,
type = ContentAttachmentData.Type.values()[it.mediaType]
@ -376,7 +380,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
displayedEventsObservable.accept(action)
if (action.event.sendState.isSent()) { //ignore pending/local events
displayedEventsObservable.accept(action)
}
//We need to update this with the related m.replace also (to move read receipt)
action.event.annotations?.editSummary?.sourceEvents?.forEach {
room.getTimeLineEvent(it)?.let { event ->

View File

@ -299,9 +299,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it }
MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) {
(MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) {
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
}).also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
}
}
}

View File

@ -39,6 +39,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
@ -90,7 +91,7 @@ class MessageItemFactory @Inject constructor(
val informationData = messageInformationDataFactory.create(event, nextEvent)
if (event.root.unsignedData?.redactedEvent != null) {
if (event.root.isRedacted()) {
//message is redacted
return buildRedactedItem(informationData, highlight, callback)
}
@ -198,7 +199,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data(
filename = messageContent.body,
url = messageContent.url,
url = messageContent.encryptedFileInfo?.url ?: messageContent.url,
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageContent.info?.height,
maxHeight = maxHeight,
width = messageContent.info?.width,
@ -239,10 +241,11 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body,
url = messageContent.info?.thumbnailUrl,
height = messageContent.info?.height,
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight,
width = messageContent.info?.width,
width = messageContent.videoInfo?.width,
maxWidth = maxWidth
)

View File

@ -55,7 +55,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
// Crypto
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, highlight, callback)
EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(event, nextEvent, highlight, callback)
} else {
encryptedItemFactory.create(event, nextEvent, highlight, callback)
}
}
// Unhandled event types (yet)
EventType.STATE_ROOM_THIRD_PARTY_INVITE,

View File

@ -31,4 +31,19 @@ class TimelineEventVisibilityStateChangedListener(private val callback: Timeline
}
}
}
class MergedTimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
private val events: List<TimelineEvent>)
: VectorEpoxyModel.OnVisibilityStateChangedListener {
override fun onVisibilityStateChanged(visibilityState: Int) {
if (visibilityState == VisibilityState.VISIBLE) {
events.forEach {
callback?.onEventVisible(it)
}
}
}
}

View File

@ -22,8 +22,8 @@ import android.widget.ImageView
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.github.piasy.biv.view.BigImageView
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
@ -37,6 +37,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
data class Data(
val filename: String,
val url: String?,
val elementToDecrypt: ElementToDecrypt?,
val height: Int?,
val maxHeight: Int,
val width: Int?,
@ -59,17 +60,28 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
val (width, height) = processSize(data, mode)
imageView.layoutParams.height = height
imageView.layoutParams.width = width
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) {
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
//Fallback to base url
?: data.url
GlideApp
.with(imageView)
.load(resolvedUrl)
val glideRequest = if (data.elementToDecrypt != null) {
// Encrypted image
GlideApp
.with(imageView)
.load(data)
} else {
// Clear image
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) {
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
//Fallback to base url
?: data.url
GlideApp
.with(imageView)
.load(resolvedUrl)
}
glideRequest
.dontAnimate()
.transform(RoundedCorners(dpToPx(8, imageView.context)))
.thumbnail(0.3f)
@ -81,6 +93,8 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val fullSize = contentUrlResolver.resolveFullSize(data.url)
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
// TODO DECRYPT_FILE Decrypt file
imageView.showImage(
Uri.parse(thumbnail),
Uri.parse(fullSize)

View File

@ -20,9 +20,11 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
import com.github.piasy.biv.view.GlideImageViewFactory
import im.vector.riotredesign.core.di.ScreenComponent
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_image_media_viewer.*
import javax.inject.Inject
@ -44,9 +46,26 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
finish()
} else {
configureToolbar(imageMediaViewerToolbar, mediaData)
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
imageContentRenderer.render(mediaData, imageMediaViewerImageView)
if (mediaData.elementToDecrypt != null) {
// Encrypted image
imageMediaViewerImageView.isVisible = false
encryptedImageView.isVisible = true
GlideApp
.with(this)
.load(mediaData)
.dontAnimate()
.into(encryptedImageView)
} else {
// Clear image
imageMediaViewerImageView.isVisible = true
encryptedImageView.isVisible = false
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
imageContentRenderer.render(mediaData, imageMediaViewerImageView)
}
}
}

View File

@ -26,6 +26,7 @@ import javax.inject.Inject
class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){
// TODO DECRYPT_FILE Encrypted data
@Parcelize
data class Data(
val filename: String,

View File

@ -21,10 +21,10 @@ import android.graphics.Bitmap
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.util.SecretStoringUtils
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.core.utils.SecretStoringUtils
import im.vector.riotredesign.features.settings.PreferencesManager
import me.gujun.android.span.span
import timber.log.Timber

View File

@ -32,6 +32,7 @@ import androidx.core.view.isVisible
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import com.bumptech.glide.Glide
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import im.vector.riotredesign.R
@ -46,6 +47,10 @@ import im.vector.riotredesign.core.utils.toast
import im.vector.riotredesign.features.MainActivity
import im.vector.riotredesign.features.themes.ThemeUtils
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference
import java.util.*
@ -197,6 +202,18 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notImplemented()
// TODO DECRYPT_FILE Quick implementation of clear cache, finish this
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
Glide.get(requireContext()).clearMemory()
withContext(Dispatchers.IO) {
// On BG thread
Glide.get(requireContext()).clearDiskCache()
}
}
/* TODO
displayLoadingView()

View File

@ -119,7 +119,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() {
context?.let { context: Context ->
AlertDialog.Builder(context)
.setSingleChoiceItems(R.array.media_saving_choice,
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
PreferencesManager.setSelectedMediasSavingPeriod(activity, n)
d.cancel()

View File

@ -18,6 +18,7 @@ package im.vector.riotredesign.features.workers.signout
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
@ -41,16 +42,17 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.DaggerScreenComponent
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.core.utils.toast
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import javax.inject.Inject
class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
@Inject lateinit var session: Session
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var session: Session
lateinit var viewModelFactory: ViewModelProvider.Factory
@BindView(R.id.bottom_sheet_signout_warning_text)
@ -99,6 +101,14 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
private lateinit var viewModel: SignOutViewModel
override fun onAttach(context: Context) {
super.onAttach(context)
val vectorBaseActivity = activity as VectorBaseActivity
val screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
viewModelFactory = screenComponent.viewModelFactory()
session = screenComponent.session()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -18,4 +19,11 @@
app:failureImageInitScaleType="center"
app:optimizeDisplay="true" />
<ImageView
android:id="@+id/encryptedImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>