Add baresip:// provisioning link support via encrypted bundle fetch
This commit is contained in:
@ -30,7 +30,9 @@ configure<ApplicationExtension> {
|
|||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
ndk { abiFilters.add("x86_64") }
|
ndk {
|
||||||
|
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
|
|||||||
@ -85,6 +85,12 @@
|
|||||||
<action android:name="android.intent.action.DIAL" />
|
<action android:name="android.intent.action.DIAL" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="baresip" />
|
||||||
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.intent.action.DIAL" />
|
<action android:name="android.intent.action.DIAL" />
|
||||||
|
|||||||
@ -31,6 +31,8 @@ import androidx.activity.viewModels
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
@ -176,20 +178,26 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
atStartup = intent.hasExtra("onStartup")
|
atStartup = intent.hasExtra("onStartup")
|
||||||
|
|
||||||
when (intent?.action) {
|
if (intent?.action == Intent.ACTION_VIEW &&
|
||||||
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
|
"baresip".equals(intent.data?.scheme, ignoreCase = true)
|
||||||
if (BaresipService.isServiceRunning)
|
) {
|
||||||
callAction(
|
handleProvisioningIntent(this, viewModel, intent)
|
||||||
applicationContext,
|
} else {
|
||||||
viewModel,
|
when (intent?.action) {
|
||||||
intent.data,
|
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
|
||||||
if (intent?.action == ACTION_CALL) "call" else "dial"
|
if (BaresipService.isServiceRunning)
|
||||||
)
|
callAction(
|
||||||
else
|
applicationContext,
|
||||||
BaresipService.callActionUri = intent.data
|
viewModel,
|
||||||
.toString().replace("%2B", "+")
|
intent.data,
|
||||||
.replace("%20", "")
|
if (intent?.action == ACTION_CALL) "call" else "dial"
|
||||||
.filterNot{setOf('-', ' ', '(', ')').contains(it)}
|
)
|
||||||
|
else
|
||||||
|
BaresipService.callActionUri = intent.data
|
||||||
|
.toString().replace("%2B", "+")
|
||||||
|
.replace("%20", "")
|
||||||
|
.filterNot{setOf('-', ' ', '(', ')').contains(it)}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val permissions = if (Build.VERSION.SDK_INT >= 33)
|
val permissions = if (Build.VERSION.SDK_INT >= 33)
|
||||||
@ -370,6 +378,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
isCallLogIntent(intent) -> {
|
isCallLogIntent(intent) -> {
|
||||||
handleCallLogIntent()
|
handleCallLogIntent()
|
||||||
}
|
}
|
||||||
|
intent.action == Intent.ACTION_VIEW &&
|
||||||
|
intent.data?.scheme.equals("baresip", ignoreCase = true) -> {
|
||||||
|
handleProvisioningIntent(applicationContext, viewModel, intent)
|
||||||
|
}
|
||||||
intent.action in listOf(ACTION_DIAL, ACTION_CALL, ACTION_VIEW) -> {
|
intent.action in listOf(ACTION_DIAL, ACTION_CALL, ACTION_VIEW) -> {
|
||||||
callAction(
|
callAction(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
@ -446,4 +458,36 @@ class MainActivity : ComponentActivity() {
|
|||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleProvisioningIntent(ctx: Context, viewModel: ViewModel, intent: Intent) {
|
||||||
|
val data = intent.data ?: run {
|
||||||
|
Log.w(TAG, "Provisioning intent has no data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val endpoint = data.getQueryParameter("endpoint") ?: return
|
||||||
|
val extension = data.getQueryParameter("extension") ?: return
|
||||||
|
Log.i(TAG, "Provisioning from $endpoint ext=$extension")
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val payload = Provisioning.fetchAndDecryptBundle(ctx, endpoint, extension)
|
||||||
|
val acc = Provisioning.applyBundle(ctx, payload)
|
||||||
|
if (acc == null) throw Provisioning.ProvisioningException("Failed to apply bundle")
|
||||||
|
Log.i(TAG, "Provisioning applied: ${acc.aor}")
|
||||||
|
viewModel.navigateToHome()
|
||||||
|
if (BaresipService.isServiceRunning) {
|
||||||
|
val restart = Intent(ctx, BaresipService::class.java)
|
||||||
|
restart.action = "Stop"
|
||||||
|
ContextCompat.startForegroundService(ctx, restart)
|
||||||
|
} else {
|
||||||
|
val restartApp = Intent(ctx, MainActivity::class.java)
|
||||||
|
restartApp.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
ctx.startActivity(restartApp)
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
} catch (e: Provisioning.ProvisioningException) {
|
||||||
|
Log.e(TAG, "Provisioning failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
314
app/src/main/kotlin/com/tutpro/baresip/Provisioning.kt
Normal file
314
app/src/main/kotlin/com/tutpro/baresip/Provisioning.kt
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
package com.tutpro.baresip
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.PublicKey
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
|
object Provisioning {
|
||||||
|
|
||||||
|
const val ALIAS = "baresip_provisioning"
|
||||||
|
private const val RSA_KEY_SIZE = 2048
|
||||||
|
private const val GCM_TAG_LENGTH = 128
|
||||||
|
private const val SERVER_TIMEOUT_MS = 20_000
|
||||||
|
|
||||||
|
data class BundlePayload(
|
||||||
|
val connectString: String,
|
||||||
|
val clientCertPem: String? = null,
|
||||||
|
val clientKeyPem: String? = null,
|
||||||
|
val caCertsPem: String? = null,
|
||||||
|
val sipVerifyServer: String? = null,
|
||||||
|
val sipTransport: String? = null,
|
||||||
|
val accountName: String? = null,
|
||||||
|
val username: String? = null,
|
||||||
|
val password: String? = null,
|
||||||
|
val displayName: String? = null,
|
||||||
|
val outbound1: String? = null,
|
||||||
|
val outbound2: String? = null,
|
||||||
|
val register: Boolean? = null,
|
||||||
|
val regInt: Int? = null,
|
||||||
|
val checkOrigin: Boolean? = null,
|
||||||
|
val mediaEnc: String? = null,
|
||||||
|
val mediaNat: String? = null,
|
||||||
|
val stunServer: String? = null,
|
||||||
|
val stunUser: String? = null,
|
||||||
|
val stunPass: String? = null,
|
||||||
|
val rtcpMux: Boolean? = null,
|
||||||
|
val rel100: Boolean? = null,
|
||||||
|
val dtmfMode: Int? = null,
|
||||||
|
val answerMode: Int? = null,
|
||||||
|
val autoRedirect: Boolean? = null,
|
||||||
|
val vmUri: String? = null,
|
||||||
|
val countryCode: String? = null,
|
||||||
|
val telProvider: String? = null,
|
||||||
|
val numericKeypad: Boolean? = null,
|
||||||
|
val defaultAccount: Boolean? = null,
|
||||||
|
val customParams: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun hasKeyPair(): Boolean {
|
||||||
|
return try {
|
||||||
|
val ks = KeyStore.getInstance("AndroidKeyStore")
|
||||||
|
ks.load(null)
|
||||||
|
ks.containsAlias(ALIAS)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "hasKeyPair check failed: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun ensureKeyPair(): PublicKey = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val ks = KeyStore.getInstance("AndroidKeyStore")
|
||||||
|
ks.load(null)
|
||||||
|
if (ks.containsAlias(ALIAS)) {
|
||||||
|
val pub = ks.getCertificate(ALIAS).publicKey
|
||||||
|
return@withContext pub
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "ensureKeyPair reload failed: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA)
|
||||||
|
val spec = KeyGenParameterSpec.Builder(
|
||||||
|
ALIAS,
|
||||||
|
KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_ENCRYPT
|
||||||
|
)
|
||||||
|
.setAlgorithmParameterSpec(java.security.spec.RSAKeyGenParameterSpec(RSA_KEY_SIZE, java.security.spec.RSAKeyGenParameterSpec.F4))
|
||||||
|
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
|
||||||
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
|
||||||
|
.setRandomizedEncryptionRequired(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
kpg.initialize(spec)
|
||||||
|
val kp = kpg.generateKeyPair()
|
||||||
|
kp.public
|
||||||
|
}
|
||||||
|
|
||||||
|
fun publicKeyPem(publicKey: PublicKey): String {
|
||||||
|
val b64 = Base64.encodeToString(publicKey.encoded, Base64.NO_WRAP)
|
||||||
|
return "-----BEGIN PUBLIC KEY-----\n$b64\n-----END PUBLIC KEY-----"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchAndDecryptBundle(
|
||||||
|
ctx: Context,
|
||||||
|
endpoint: String,
|
||||||
|
extension: String
|
||||||
|
): BundlePayload = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val pub = ensureKeyPair()
|
||||||
|
val enrollment = JSONObject()
|
||||||
|
enrollment.put("extension", extension)
|
||||||
|
enrollment.put("public_key", Base64.encodeToString(pub.encoded, Base64.NO_WRAP))
|
||||||
|
|
||||||
|
val url = Uri.parse(endpoint).buildUpon()
|
||||||
|
.appendPath("bundle")
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
val conn = URL(url).openConnection() as HttpsURLConnection
|
||||||
|
conn.requestMethod = "POST"
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json")
|
||||||
|
conn.doOutput = true
|
||||||
|
conn.connectTimeout = SERVER_TIMEOUT_MS
|
||||||
|
conn.readTimeout = SERVER_TIMEOUT_MS
|
||||||
|
conn.outputStream.use { os ->
|
||||||
|
os.write(enrollment.toString().toByteArray(Charsets.UTF_8))
|
||||||
|
os.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
val code = conn.responseCode
|
||||||
|
if (code !in 200..299) {
|
||||||
|
val err = conn.errorStream?.bufferedReader()?.use { it.readText() } ?: "HTTP $code"
|
||||||
|
throw ProvisioningException("Provisioning failed: $err")
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = conn.inputStream.bufferedReader().use { it.readText() }
|
||||||
|
val root = JSONObject(body)
|
||||||
|
val encryptedKeyB64 = root.optString("encrypted_key")
|
||||||
|
?: throw ProvisioningException("Missing encrypted_key")
|
||||||
|
val ivB64 = root.optString("iv")
|
||||||
|
?: throw ProvisioningException("Missing iv")
|
||||||
|
val ciphertextB64 = root.optString("ciphertext")
|
||||||
|
?: throw ProvisioningException("Missing ciphertext")
|
||||||
|
val tagB64 = root.optString("tag")
|
||||||
|
?: throw ProvisioningException("Missing tag")
|
||||||
|
|
||||||
|
val encryptedKey = Base64.decode(encryptedKeyB64, Base64.NO_WRAP)
|
||||||
|
val iv = Base64.decode(ivB64, Base64.NO_WRAP)
|
||||||
|
val ciphertext = Base64.decode(ciphertextB64, Base64.NO_WRAP)
|
||||||
|
val tag = Base64.decode(tagB64, Base64.NO_WRAP)
|
||||||
|
|
||||||
|
val aesKey = rsaDecrypt(encryptedKey)
|
||||||
|
val plaintext = aesGcmDecrypt(aesKey, iv, ciphertext, tag)
|
||||||
|
val payload = JSONObject(String(plaintext, Charsets.UTF_8))
|
||||||
|
val connectString = payload.optString("connect_string")
|
||||||
|
?: throw ProvisioningException("Missing connect_string in bundle")
|
||||||
|
|
||||||
|
BundlePayload(
|
||||||
|
connectString = connectString,
|
||||||
|
clientCertPem = payload.optString("client_cert", null),
|
||||||
|
clientKeyPem = payload.optString("client_key", null),
|
||||||
|
caCertsPem = payload.optString("ca_certs", null),
|
||||||
|
sipVerifyServer = payload.optString("sip_verify_server"),
|
||||||
|
sipTransport = payload.optString("transport"),
|
||||||
|
accountName = payload.optString("account_name"),
|
||||||
|
username = payload.optString("username"),
|
||||||
|
password = payload.optString("password"),
|
||||||
|
displayName = payload.optString("display_name"),
|
||||||
|
outbound1 = payload.optString("outbound1"),
|
||||||
|
outbound2 = payload.optString("outbound2"),
|
||||||
|
register = if (payload.has("register")) payload.getBoolean("register") else null,
|
||||||
|
regInt = if (payload.has("reg_int")) payload.getInt("reg_int") else null,
|
||||||
|
checkOrigin = if (payload.has("check_origin")) payload.getBoolean("check_origin") else null,
|
||||||
|
mediaEnc = payload.optString("media_enc", null),
|
||||||
|
mediaNat = payload.optString("media_nat", null),
|
||||||
|
stunServer = payload.optString("stun_server", null),
|
||||||
|
stunUser = payload.optString("stun_user", null),
|
||||||
|
stunPass = payload.optString("stun_pass", null),
|
||||||
|
rtcpMux = if (payload.has("rtcp_mux")) payload.getBoolean("rtcp_mux") else null,
|
||||||
|
rel100 = if (payload.has("rel100")) payload.getBoolean("rel100") else null,
|
||||||
|
dtmfMode = if (payload.has("dtmf_mode")) payload.getInt("dtmf_mode") else null,
|
||||||
|
answerMode = if (payload.has("answer_mode")) payload.getInt("answer_mode") else null,
|
||||||
|
autoRedirect = if (payload.has("auto_redirect")) payload.getBoolean("auto_redirect") else null,
|
||||||
|
vmUri = payload.optString("vm_uri", null),
|
||||||
|
countryCode = payload.optString("country_code", null),
|
||||||
|
telProvider = payload.optString("tel_provider", null),
|
||||||
|
numericKeypad = if (payload.has("numeric_keypad")) payload.getBoolean("numeric_keypad") else null,
|
||||||
|
defaultAccount = if (payload.has("default_account")) payload.getBoolean("default_account") else null,
|
||||||
|
customParams = payload.optString("custom_params", null),
|
||||||
|
)
|
||||||
|
} catch (e: ProvisioningException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Provisioning fetch failed", e)
|
||||||
|
throw ProvisioningException("Provisioning error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun applyBundle(ctx: Context, payload: BundlePayload): Account? = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val filesPath = BaresipService.filesPath
|
||||||
|
val file = File(filesPath)
|
||||||
|
if (!file.exists()) file.mkdirs()
|
||||||
|
|
||||||
|
var connectString = payload.connectString.trim()
|
||||||
|
val transport = payload.sipTransport?.trim()?.lowercase()
|
||||||
|
if ((transport == "tls" || transport == "wss") && !connectString.contains("transport=")) {
|
||||||
|
connectString = if (connectString.contains("?")) {
|
||||||
|
"$connectString&transport=$transport"
|
||||||
|
} else {
|
||||||
|
"$connectString;transport=$transport"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val existing = Account.ofAor(connectString)
|
||||||
|
val ua = if (existing != null) UserAgent.ofAor(existing.aor) else null
|
||||||
|
val acc = ua?.account ?: run {
|
||||||
|
val aor = if (connectString.startsWith("sip:")) connectString else "sip:$connectString"
|
||||||
|
val uap = UserAgent.uaAlloc(
|
||||||
|
"<$aor>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;check_origin=no;mwi=no"
|
||||||
|
)
|
||||||
|
if (uap == 0L) return@withContext null
|
||||||
|
UserAgent.ofUap(uap)?.account ?: return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.displayName?.let { acc.displayName = it }
|
||||||
|
payload.username?.let { acc.authUser = it }
|
||||||
|
if (!payload.password.isNullOrBlank()) {
|
||||||
|
BaresipService.aorPasswords[acc.aor] = payload.password
|
||||||
|
}
|
||||||
|
payload.outbound1?.let { if (acc.outbound.size > 0) acc.outbound[0] = it else acc.outbound.add(it) }
|
||||||
|
payload.outbound2?.let { if (acc.outbound.size > 1) acc.outbound[1] = it else if (it.isNotBlank()) acc.outbound.add(it) }
|
||||||
|
if (payload.register != null) {
|
||||||
|
acc.regint = if (payload.register) payload.regInt ?: REGISTRATION_INTERVAL else 0
|
||||||
|
acc.configuredRegInt = payload.regInt ?: REGISTRATION_INTERVAL
|
||||||
|
}
|
||||||
|
payload.checkOrigin?.let { acc.checkOrigin = it }
|
||||||
|
payload.mediaEnc?.let { acc.mediaEnc = it }
|
||||||
|
payload.mediaNat?.let { acc.mediaNat = it }
|
||||||
|
payload.stunServer?.let { acc.stunServer = it }
|
||||||
|
payload.stunUser?.let { acc.stunUser = it }
|
||||||
|
payload.stunPass?.let { acc.stunPass = it }
|
||||||
|
payload.rtcpMux?.let { acc.rtcpMux = it }
|
||||||
|
if (payload.rel100 != null) {
|
||||||
|
acc.rel100Mode = if (payload.rel100) Api.REL100_ENABLED else Api.REL100_DISABLED
|
||||||
|
}
|
||||||
|
payload.dtmfMode?.let { acc.dtmfMode = it }
|
||||||
|
payload.answerMode?.let { acc.answerMode = it }
|
||||||
|
payload.autoRedirect?.let { acc.autoRedirect = it }
|
||||||
|
payload.vmUri?.let { acc.vmUri = it }
|
||||||
|
payload.countryCode?.let { acc.countryCode = it }
|
||||||
|
payload.telProvider?.let { acc.telProvider = it }
|
||||||
|
payload.numericKeypad?.let { acc.numericKeypad = it }
|
||||||
|
payload.defaultAccount?.let { if (it) UserAgent.ofAor(acc.aor)?.makeDefault() }
|
||||||
|
payload.customParams?.let { acc.customParams = it }
|
||||||
|
payload.accountName?.let { acc.nickName = it }
|
||||||
|
|
||||||
|
payload.caCertsPem?.let {
|
||||||
|
File("$filesPath/ca_certs.crt").writeText(it, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.clientCertPem?.let { cert ->
|
||||||
|
File("$filesPath/cert.pem").writeText(cert, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.clientKeyPem?.let { key ->
|
||||||
|
File("$filesPath/cert_key.pem").writeText(key, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.sipVerifyServer.isNullOrBlank()) {
|
||||||
|
val config = File("$filesPath/config")
|
||||||
|
if (config.exists()) {
|
||||||
|
val cfg = config.readText(Charsets.UTF_8)
|
||||||
|
config.writeText(cfg.replace("sip_verify_server ${Config.variable("sip_verify_server")}", "sip_verify_server ${payload.sipVerifyServer}"), Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Account.saveAccounts()
|
||||||
|
UserAgent.ofAor(acc.aor)?.let { if (acc.regint > 0) it.reRegister() }
|
||||||
|
}
|
||||||
|
|
||||||
|
acc
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to apply bundle", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rsaDecrypt(encrypted: ByteArray): ByteArray {
|
||||||
|
val ks = KeyStore.getInstance("AndroidKeyStore")
|
||||||
|
ks.load(null)
|
||||||
|
val privateKey = ks.getKey(ALIAS, null) as PrivateKey
|
||||||
|
val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
||||||
|
return cipher.doFinal(encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aesGcmDecrypt(key: ByteArray, iv: ByteArray, ciphertext: ByteArray, tag: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(GCM_TAG_LENGTH, iv))
|
||||||
|
val combined = ByteArray(ciphertext.size + tag.size)
|
||||||
|
System.arraycopy(ciphertext, 0, combined, 0, ciphertext.size)
|
||||||
|
System.arraycopy(tag, 0, combined, ciphertext.size, tag.size)
|
||||||
|
return cipher.doFinal(combined)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProvisioningException(message: String) : Exception(message)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user