Этап 0 п.3: безопасность-минимум (trust-all off в release, шифрование пароля, allowBackup, ревокация)
1. trust-all TLS отключён в release: гейт if(!BuildConfig.DEBUG) в UnsafeSsl + OfficeWebViewClient + TalkCallActivity; чекбокс скрыт в release. buildConfig включён в core:network и feature:talk. 2. allowBackup=false (tools:replace поверх vendor true). 3. app password шифруется в покое AndroidKeystore AES-256-GCM (KeystoreCrypto), AuthStore мигрирует legacy-plaintext при первом load. Биометрия — отдельная задача (ломает фоновые push/звонки, нужен кэш в памяти). 4. Ревокация app password при logout (AppPasswordRevoker, DELETE core/apppassword, best-effort). Проверено: assembleRelease 53 МБ, подпись v2+v3, allowBackup=false в манифесте OK.
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск`
|
Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск`
|
||||||
|
|
||||||
|
- 2026-07-07 | v0.5.114 (122) | Этап 0 п.3 (безопасность-минимум): (1) trust-all TLS отключён в release — гейт `if(!BuildConfig.DEBUG)` в UnsafeSsl + обе WebView-точки (OfficeWebViewClient, TalkCallActivity), чекбокс «доверять сертификату» скрыт в release (buildConfig включён в core:network и feature:talk); (2) allowBackup=false (переопределяет vendor true, подтверждено в собранном манифесте); (3) app password шифруется в покое ключом AndroidKeystore AES-256-GCM (KeystoreCrypto) + одноразовая миграция legacy-plaintext при load; (4) ревокация app password при logout (AppPasswordRevoker: DELETE /ocs/v2.php/core/apppassword, best-effort). Биометрическая привязка ключа — отдельная задача (конфликтует с фоновыми push/звонками, нужен кэш в памяти). Проверено сборкой: assembleRelease 53 МБ, подпись v2+v3, allowBackup=false OK | НОВЫЙ вызов OCS: DELETE core/apppassword при logout (стандартный NC endpoint) | средний: миграция хранилища и logout затрагивают все сессии — проверить на устройстве вход/выход/перезапуск
|
||||||
- 2026-07-07 | v0.5.114 (122) | Этап 0 п.2 (размер APK): универсальный APK — abiFilters 'arm64-v8a','armeabi-v7a' (без эмуляторных x86/x86_64) + resourceConfigurations ['ru','en'] (у vendor было ~48 локалей). Проверено сборкой: assembleRelease → 53 МБ (было 99 с 4 ABI; ~130 debug), ABI внутри только ARM, подпись v2+v3 OK. RenderScript в vendor НЕ трогал (риск blur-эффектов, отдельный пункт) | контракты не менялись | низкий: только упаковка; функционал не тронут
|
- 2026-07-07 | v0.5.114 (122) | Этап 0 п.2 (размер APK): универсальный APK — abiFilters 'arm64-v8a','armeabi-v7a' (без эмуляторных x86/x86_64) + resourceConfigurations ['ru','en'] (у vendor было ~48 локалей). Проверено сборкой: assembleRelease → 53 МБ (было 99 с 4 ABI; ~130 debug), ABI внутри только ARM, подпись v2+v3 OK. RenderScript в vendor НЕ трогал (риск blur-эффектов, отдельный пункт) | контракты не менялись | низкий: только упаковка; функционал не тронут
|
||||||
- 2026-07-07 | v0.5.114 (122) | Аудит + Этап 0 п.1 (release собираем и подписан): добавлен signingConfig (ключ из env F7_KEYSTORE_*/-P, фолбэк на debug); написаны R8-правила — app/proguard-rules.pro (kotlinx.serialization/Firebase/JS-мосты) + vendor consumer-rules.pro через consumerProguardFiles (WebRTC/LoganSquare/Dagger/RxJava/Parceler/EventBus + dontwarn geogson/joda); подпись v2+v3. Проверено локально (JDK 21 + Android SDK 36 поставлены на инфру B): assembleRelease → BUILD SUCCESSFUL, подписанный APK 99 МБ (было ~130 debug). Полный аудит — docs/AUDIT-2026-07-07.md; подпись — docs/RELEASE-SIGNING.md | контракты не менялись; сборочный контур зафиксирован | низкий: код приложения не тронут, изменения только в сборке; APK всё ещё 4 ABI (ABI-split — п.2)
|
- 2026-07-07 | v0.5.114 (122) | Аудит + Этап 0 п.1 (release собираем и подписан): добавлен signingConfig (ключ из env F7_KEYSTORE_*/-P, фолбэк на debug); написаны R8-правила — app/proguard-rules.pro (kotlinx.serialization/Firebase/JS-мосты) + vendor consumer-rules.pro через consumerProguardFiles (WebRTC/LoganSquare/Dagger/RxJava/Parceler/EventBus + dontwarn geogson/joda); подпись v2+v3. Проверено локально (JDK 21 + Android SDK 36 поставлены на инфру B): assembleRelease → BUILD SUCCESSFUL, подписанный APK 99 МБ (было ~130 debug). Полный аудит — docs/AUDIT-2026-07-07.md; подпись — docs/RELEASE-SIGNING.md | контракты не менялись; сборочный контур зафиксирован | низкий: код приложения не тронут, изменения только в сборке; APK всё ещё 4 ABI (ABI-split — п.2)
|
||||||
- 2026-07-07 | v0.5.114 (122) | Санитария по mail/035: дефолт-URL входа → BuildConfig.DEFAULT_SERVER_URL (свойство f7DefaultServerUrl, по умолчанию пусто); убран preconnect к боевому hpb-prod.f7cloud.ru (TalkHelper); нейтральный пример URL в тексте ошибки | INTEGRATION.md: зашитых касаний хостов A больше нет | низкий: preconnect — только оптимизация; сборка на билд-сервере ещё не прогонялась
|
- 2026-07-07 | v0.5.114 (122) | Санитария по mail/035: дефолт-URL входа → BuildConfig.DEFAULT_SERVER_URL (свойство f7DefaultServerUrl, по умолчанию пусто); убран preconnect к боевому hpb-prod.f7cloud.ru (TalkHelper); нейтральный пример URL в тексте ошибки | INTEGRATION.md: зашитых касаний хостов A больше нет | низкий: preconnect — только оптимизация; сборка на билд-сервере ещё не прогонялась
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ legacy WebView-оболочки `android-webview`.
|
|||||||
| Контракт | Что используется |
|
| Контракт | Что используется |
|
||||||
|---|---|
|
|---|---|
|
||||||
| OCS `/ocs/v2.php/cloud/user` | авторизация, `davUserId` (пароль/app-password при 2FA) |
|
| OCS `/ocs/v2.php/cloud/user` | авторизация, `davUserId` (пароль/app-password при 2FA) |
|
||||||
|
| OCS `DELETE /ocs/v2.php/core/apppassword` | ревокация app password при logout (best-effort) |
|
||||||
| WebDAV | файлы (PROPFIND, `oc:fileid`) |
|
| WebDAV | файлы (PROPFIND, `oc:fileid`) |
|
||||||
| CalDAV | календарь и задачи |
|
| CalDAV | календарь и задачи |
|
||||||
| CardDAV | контакты |
|
| CardDAV | контакты |
|
||||||
|
|||||||
@@ -22,13 +22,13 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".F7MobileApp"
|
android:name=".F7MobileApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@android:style/Theme.Material.Light.NoActionBar"
|
android:theme="@android:style/Theme.Material.Light.NoActionBar"
|
||||||
tools:replace="android:label,android:theme,android:icon,android:roundIcon">
|
tools:replace="android:label,android:theme,android:icon,android:roundIcon,android:allowBackup">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
android:value="f7cloud_messages_v2" />
|
android:value="f7cloud_messages_v2" />
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ internal class OfficeWebViewClient(
|
|||||||
handler: SslErrorHandler?,
|
handler: SslErrorHandler?,
|
||||||
error: SslError?,
|
error: SslError?,
|
||||||
) {
|
) {
|
||||||
if (launch.trustAllCerts) {
|
// trust-all TLS в WebView — только debug (см. UnsafeSsl); в release всегда штатная проверка.
|
||||||
|
if (launch.trustAllCerts && BuildConfig.DEBUG) {
|
||||||
handler?.proceed()
|
handler?.proceed()
|
||||||
} else {
|
} else {
|
||||||
super.onReceivedSslError(view, handler, error)
|
super.onReceivedSslError(view, handler, error)
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import ru.forbion.f7cloud.mobile.BuildConfig
|
import ru.forbion.f7cloud.mobile.BuildConfig
|
||||||
import ru.forbion.f7cloud.core.auth.AppLockStore
|
import ru.forbion.f7cloud.core.auth.AppLockStore
|
||||||
|
import ru.forbion.f7cloud.core.auth.AppPasswordRevoker
|
||||||
import ru.forbion.f7cloud.core.auth.AuthSession
|
import ru.forbion.f7cloud.core.auth.AuthSession
|
||||||
import ru.forbion.f7cloud.core.auth.AuthStore
|
import ru.forbion.f7cloud.core.auth.AuthStore
|
||||||
import ru.forbion.f7cloud.core.auth.AuthVerifier
|
import ru.forbion.f7cloud.core.auth.AuthVerifier
|
||||||
@@ -172,7 +173,12 @@ fun AppScaffold(
|
|||||||
context.applicationContext.getSharedPreferences("f7_permissions", android.content.Context.MODE_PRIVATE)
|
context.applicationContext.getSharedPreferences("f7_permissions", android.content.Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val logoutScope = rememberCoroutineScope()
|
||||||
val forceLogout = {
|
val forceLogout = {
|
||||||
|
// Best-effort ревокация app password на сервере ДО очистки локальной сессии.
|
||||||
|
session?.let { current ->
|
||||||
|
logoutScope.launch { AppPasswordRevoker.revoke(current) }
|
||||||
|
}
|
||||||
OfficeWarmup.clear()
|
OfficeWarmup.clear()
|
||||||
OfficeWebViewPool.dispose()
|
OfficeWebViewPool.dispose()
|
||||||
authStore.clear()
|
authStore.clear()
|
||||||
@@ -873,19 +879,23 @@ private fun LoginScreen(onLogin: (AuthSession) -> Unit) {
|
|||||||
submitLogin()
|
submitLogin()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Row(
|
// Чекбокс trust-all показываем только в debug-сборках: в release он
|
||||||
modifier = Modifier.fillMaxWidth(),
|
// всё равно игнорируется на уровне сети/WebView (см. UnsafeSsl).
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
if (BuildConfig.DEBUG) {
|
||||||
) {
|
Row(
|
||||||
Checkbox(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
checked = trustAllCerts,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
onCheckedChange = { trustAllCerts = it },
|
) {
|
||||||
)
|
Checkbox(
|
||||||
Text(
|
checked = trustAllCerts,
|
||||||
text = "Доверять сертификату (dev)",
|
onCheckedChange = { trustAllCerts = it },
|
||||||
style = MaterialTheme.typography.bodySmall,
|
)
|
||||||
color = F7Colors.TextSecondary,
|
Text(
|
||||||
)
|
text = "Доверять сертификату (dev)",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = F7Colors.TextSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
F7PrimaryButton(
|
F7PrimaryButton(
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ru.forbion.f7cloud.core.auth
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Request
|
||||||
|
import ru.forbion.f7cloud.core.network.NetworkFactory
|
||||||
|
import ru.forbion.f7cloud.core.network.applyOcsJson
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ревокация app password на сервере при выходе (`DELETE /ocs/v2.php/core/apppassword`).
|
||||||
|
*
|
||||||
|
* Best-effort: если офлайн/ошибка — выход всё равно проходит (локальные данные чистятся
|
||||||
|
* вызывающим), протухший токен не критичен. Вызывать ДО [AuthStore.clear].
|
||||||
|
*/
|
||||||
|
object AppPasswordRevoker {
|
||||||
|
suspend fun revoke(session: AuthSession): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val client = NetworkFactory.newAuthedClient(
|
||||||
|
session.username,
|
||||||
|
session.appPassword,
|
||||||
|
session.trustAllCerts,
|
||||||
|
)
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("${session.serverUrl.trimEnd('/')}/ocs/v2.php/core/apppassword?format=json")
|
||||||
|
.applyOcsJson()
|
||||||
|
.delete()
|
||||||
|
.build()
|
||||||
|
client.newCall(request).execute().use { it.isSuccessful }
|
||||||
|
}.getOrDefault(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ class AuthStore(context: Context) {
|
|||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("server_url", session.serverUrl.trimEnd('/'))
|
.putString("server_url", session.serverUrl.trimEnd('/'))
|
||||||
.putString("username", session.username.trim())
|
.putString("username", session.username.trim())
|
||||||
.putString("app_password", session.appPassword)
|
// app password шифруется в покое ключом из AndroidKeystore (см. KeystoreCrypto)
|
||||||
|
.putString("app_password", KeystoreCrypto.encrypt(session.appPassword))
|
||||||
.putBoolean("trust_all_certs", session.trustAllCerts)
|
.putBoolean("trust_all_certs", session.trustAllCerts)
|
||||||
.putString("dav_user_id", session.davUserId)
|
.putString("dav_user_id", session.davUserId)
|
||||||
.apply()
|
.apply()
|
||||||
@@ -18,14 +19,28 @@ class AuthStore(context: Context) {
|
|||||||
fun load(): AuthSession? {
|
fun load(): AuthSession? {
|
||||||
val serverUrl = prefs.getString("server_url", null) ?: return null
|
val serverUrl = prefs.getString("server_url", null) ?: return null
|
||||||
val username = prefs.getString("username", null) ?: return null
|
val username = prefs.getString("username", null) ?: return null
|
||||||
val appPassword = prefs.getString("app_password", null) ?: return null
|
val storedPassword = prefs.getString("app_password", null) ?: return null
|
||||||
return AuthSession(
|
|
||||||
|
val appPassword = if (KeystoreCrypto.isEncrypted(storedPassword)) {
|
||||||
|
// Расшифровка может упасть, если Keystore-ключ инвалидирован (сброс учётных
|
||||||
|
// данных устройства) — трактуем как «сессии нет», пользователь войдёт заново.
|
||||||
|
runCatching { KeystoreCrypto.decrypt(storedPassword) }.getOrNull() ?: return null
|
||||||
|
} else {
|
||||||
|
// Legacy: пароль сохранён plaintext (до шифрования) — используем и МИГРИРУЕМ ниже.
|
||||||
|
storedPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
val session = AuthSession(
|
||||||
serverUrl = serverUrl,
|
serverUrl = serverUrl,
|
||||||
username = username,
|
username = username,
|
||||||
appPassword = appPassword,
|
appPassword = appPassword,
|
||||||
trustAllCerts = prefs.getBoolean("trust_all_certs", false),
|
trustAllCerts = prefs.getBoolean("trust_all_certs", false),
|
||||||
davUserId = prefs.getString("dav_user_id", null),
|
davUserId = prefs.getString("dav_user_id", null),
|
||||||
)
|
)
|
||||||
|
if (!KeystoreCrypto.isEncrypted(storedPassword)) {
|
||||||
|
runCatching { save(session) } // одноразовая миграция plaintext → ciphertext
|
||||||
|
}
|
||||||
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package ru.forbion.f7cloud.core.auth
|
||||||
|
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Base64
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Шифрование секретов в покое ключом из AndroidKeystore (AES-256-GCM).
|
||||||
|
*
|
||||||
|
* Ключ НЕэкспортируемый (на большинстве устройств — аппаратный TEE/StrongBox), поэтому даже
|
||||||
|
* утёкший ciphertext (бэкап, дамп prefs, root) бесполезен вне этого устройства.
|
||||||
|
*
|
||||||
|
* Пер-запросной авторизации пользователя НЕТ намеренно (`setUserAuthenticationRequired` не
|
||||||
|
* ставится): пароль читается в фоне (push, входящие звонки, холодный старт), где биометрию
|
||||||
|
* спросить не у кого. Биометрическая привязка — отдельная задача поверх (кэш в памяти +
|
||||||
|
* деградация фоновых сценариев).
|
||||||
|
*
|
||||||
|
* Формат выхода: `v1:` + Base64(IV[12] || ciphertext+tag). Префикс отличает шифртекст от
|
||||||
|
* legacy-plaintext при миграции.
|
||||||
|
*/
|
||||||
|
object KeystoreCrypto {
|
||||||
|
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||||
|
private const val KEY_ALIAS = "f7_auth_master"
|
||||||
|
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||||
|
private const val IV_SIZE = 12
|
||||||
|
private const val TAG_BITS = 128
|
||||||
|
const val PREFIX = "v1:"
|
||||||
|
|
||||||
|
fun isEncrypted(value: String?): Boolean = value != null && value.startsWith(PREFIX)
|
||||||
|
|
||||||
|
/** Шифрует строку; результат — с префиксом [PREFIX]. */
|
||||||
|
fun encrypt(plaintext: String): String {
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
|
||||||
|
val iv = cipher.iv
|
||||||
|
val ct = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
|
||||||
|
val combined = ByteArray(iv.size + ct.size)
|
||||||
|
System.arraycopy(iv, 0, combined, 0, iv.size)
|
||||||
|
System.arraycopy(ct, 0, combined, iv.size, ct.size)
|
||||||
|
return PREFIX + Base64.encodeToString(combined, Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Расшифровывает строку с префиксом [PREFIX]. Бросает, если ключ недоступен/инвалидирован
|
||||||
|
* (например, сброшены учётные данные устройства) или данные повреждены — вызывающий
|
||||||
|
* трактует это как «сессии нет» (повторный вход).
|
||||||
|
*/
|
||||||
|
fun decrypt(stored: String): String {
|
||||||
|
require(isEncrypted(stored)) { "not an encrypted value" }
|
||||||
|
val combined = Base64.decode(stored.removePrefix(PREFIX), Base64.NO_WRAP)
|
||||||
|
val iv = combined.copyOfRange(0, IV_SIZE)
|
||||||
|
val ct = combined.copyOfRange(IV_SIZE, combined.size)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), GCMParameterSpec(TAG_BITS, iv))
|
||||||
|
return String(cipher.doFinal(ct), Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOrCreateKey(): SecretKey {
|
||||||
|
val ks = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||||
|
(ks.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry)?.let { return it.secretKey }
|
||||||
|
val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
|
||||||
|
generator.init(
|
||||||
|
KeyGenParameterSpec.Builder(
|
||||||
|
KEY_ALIAS,
|
||||||
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
|
||||||
|
)
|
||||||
|
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||||
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
|
.setKeySize(256)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
return generator.generateKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 26
|
minSdk 26
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true // нужен BuildConfig.DEBUG для гейта trust-all TLS (только debug)
|
||||||
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import javax.net.ssl.TrustManager
|
|||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
internal fun OkHttpClient.Builder.applyUnsafeSslIfNeeded(trustAllCerts: Boolean): OkHttpClient.Builder {
|
internal fun OkHttpClient.Builder.applyUnsafeSslIfNeeded(trustAllCerts: Boolean): OkHttpClient.Builder {
|
||||||
|
// trust-all (доверие любому TLS-сертификату) — ТОЛЬКО в debug-сборках. В release флаг
|
||||||
|
// игнорируется полностью, даже если сохранён в старой сессии (защита от MITM). Реальные
|
||||||
|
// тенанты F7 имеют валидный TLS; для self-signed — отдельная задача (пиннинг/user-CA).
|
||||||
|
if (!BuildConfig.DEBUG) return this
|
||||||
if (!trustAllCerts) return this
|
if (!trustAllCerts) return this
|
||||||
val trustAll = arrayOf<TrustManager>(
|
val trustAll = arrayOf<TrustManager>(
|
||||||
object : X509TrustManager {
|
object : X509TrustManager {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose true
|
compose true
|
||||||
|
buildConfig true // BuildConfig.DEBUG — гейт trust-all TLS в WebView звонка (только debug)
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
|||||||
@@ -333,7 +333,8 @@ class TalkCallActivity : ComponentActivity() {
|
|||||||
handler: SslErrorHandler?,
|
handler: SslErrorHandler?,
|
||||||
error: SslError?,
|
error: SslError?,
|
||||||
) {
|
) {
|
||||||
if (launch.trustAllCerts) {
|
// trust-all TLS — только debug (см. UnsafeSsl); в release штатная проверка.
|
||||||
|
if (launch.trustAllCerts && BuildConfig.DEBUG) {
|
||||||
handler?.proceed()
|
handler?.proceed()
|
||||||
} else {
|
} else {
|
||||||
super.onReceivedSslError(view, handler, error)
|
super.onReceivedSslError(view, handler, error)
|
||||||
|
|||||||
Reference in New Issue
Block a user