diff --git a/app/build.gradle b/app/build.gradle index 2ebc12dd0..fd0002ad2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -253,6 +253,8 @@ dependencies { // Only enable leak canary prior to release //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' + implementation "androidx.security:security-crypto:1.1.0-alpha03" + } if (firebaseEnabled()) { diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 78d9b5f1b..7c267cc35 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -21,19 +21,36 @@ package org.linphone.core import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.graphics.PixelFormat import android.media.AudioManager import android.os.Handler import android.os.Looper import android.os.Vibrator +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties import android.telephony.PhoneStateListener import android.telephony.TelephonyManager +import android.util.Base64 +import android.util.Pair import android.view.* import androidx.emoji.bundled.BundledEmojiCompatConfig import androidx.emoji.text.EmojiCompat import androidx.lifecycle.MutableLiveData +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import androidx.security.crypto.MasterKey.Builder import com.google.firebase.crashlytics.FirebaseCrashlytics import java.io.File +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.KeyStore +import java.security.MessageDigest +import java.util.* +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec import kotlin.math.abs import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R @@ -208,7 +225,9 @@ class CoreContext(val context: Context, coreConfig: Config) { if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && call.currentParams.videoEnabled()) { // Do not turn speaker on when video is enabled if headset or bluetooth is used - if (!AudioRouteUtils.isHeadsetAudioRouteAvailable() && !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed(call)) { + if (!AudioRouteUtils.isHeadsetAudioRouteAvailable() && !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed( + call + )) { Log.i("[Context] Video enabled and no wired headset not bluetooth in use, routing audio to speaker") AudioRouteUtils.routeAudioToSpeaker(call) } @@ -272,6 +291,9 @@ class CoreContext(val context: Context, coreConfig: Config) { Log.i("[Context] Crashlytics enabled, register logging service listener") } + if (corePreferences.vfsEnabled) { + setupVFS(context) + } core = Factory.instance().createCoreWithConfig(coreConfig, context) stopped = false @@ -619,4 +641,122 @@ class CoreContext(val context: Context, coreConfig: Config) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) context.startActivity(intent) } + + /* VFS */ + + companion object { + private val TRANSFORMATION = "AES/GCM/NoPadding" + private val ANDROID_KEY_STORE = "AndroidKeyStore" + private val ALIAS = "vfs" + private val LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256 = 2 + } + + @Throws(java.lang.Exception::class) + private fun generateSecretKey() { + val keyGenerator: KeyGenerator + keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE) + keyGenerator.init( + KeyGenParameterSpec.Builder( + ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() + ) + keyGenerator.generateKey() + } + + @Throws(java.lang.Exception::class) + private fun getSecretKey(): SecretKey? { + val ks = KeyStore.getInstance(ANDROID_KEY_STORE) + ks.load(null) + val entry = ks.getEntry(ALIAS, null) as KeyStore.SecretKeyEntry + return entry.secretKey + } + + @Throws(java.lang.Exception::class) + fun generateToken(): String? { + return sha512(UUID.randomUUID().toString()) + } + + @Throws(java.lang.Exception::class) + private fun encryptData(textToEncrypt: String): Pair { + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) + val iv = cipher.iv + return Pair( + iv, + cipher.doFinal(textToEncrypt.toByteArray(StandardCharsets.UTF_8)) + ) + } + + @Throws(java.lang.Exception::class) + private fun decryptData(encrypted: String?, encryptionIv: ByteArray): String { + val cipher = Cipher.getInstance(TRANSFORMATION) + val spec = GCMParameterSpec(128, encryptionIv) + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) + val encryptedData = Base64.decode(encrypted, Base64.DEFAULT) + return String(cipher.doFinal(encryptedData), StandardCharsets.UTF_8) + } + + @Throws(java.lang.Exception::class) + fun encryptToken(string_to_encrypt: String): Pair? { + val encryptedData = encryptData(string_to_encrypt) + return Pair( + Base64.encodeToString(encryptedData.first, Base64.DEFAULT), + Base64.encodeToString(encryptedData.second, Base64.DEFAULT) + ) + } + + @Throws(java.lang.Exception::class) + fun sha512(input: String): String? { + val md = MessageDigest.getInstance("SHA-512") + val messageDigest = md.digest(input.toByteArray()) + val no = BigInteger(1, messageDigest) + var hashtext = no.toString(16) + while (hashtext.length < 32) { + hashtext = "0$hashtext" + } + return hashtext + } + + @Throws(java.lang.Exception::class) + fun getVfsKey(sharedPreferences: SharedPreferences): String? { + val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE) + keyStore.load(null) + return decryptData( + sharedPreferences.getString("vfskey", null), + Base64.decode(sharedPreferences.getString("vfsiv", null), Base64.DEFAULT) + ) + } + + private fun setupVFS(c: Context) { + try { + val masterKey: MasterKey = Builder( + c, + MasterKey.DEFAULT_MASTER_KEY_ALIAS + ).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() + val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create( + c, "vfs.prefs", masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + if (sharedPreferences.getString("vfsiv", null) == null) { + generateSecretKey() + generateToken()?.let { encryptToken(it) }?.let { data -> + sharedPreferences.edit().putString("vfsiv", data.first) + .putString("vfskey", data.second).commit() + } + } + Factory.instance().setVfsEncryption( + LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256, + Arrays.copyOfRange(getVfsKey(sharedPreferences)?.toByteArray(), 0, 32), + 32 + ) + } catch (e: Exception) { + e.printStackTrace() + throw RuntimeException("Unable to setup VFS encryption") + } + } } diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 5f387c6c7..4a88d66f9 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -35,6 +35,18 @@ class CorePreferences constructor(private val context: Context) { _config = value } + /* VFS encryption */ + + var vfsEnabled: Boolean + get() = config.getBool("app", "vfs", false) + set(value) { + if (!value && config.getBool("app", "vfs", false)) { + Log.w("[VFS] It is not possible to deactivate VFS after it has been activated") + return + } + config.setBool("app", "vfs", value) + } + /* App settings */ var debugLogs: Boolean