diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt index 62926fe7..64722fde 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt @@ -3,9 +3,14 @@ package com.tutpro.baresip import android.app.Activity import android.content.Intent import android.os.Bundle +import android.os.Environment import android.support.v7.app.AppCompatActivity +import android.view.Menu import android.view.MenuItem import android.widget.* +import android.app.AlertDialog +import android.view.ViewGroup +import android.view.LayoutInflater import java.io.File import java.util.ArrayList @@ -13,8 +18,8 @@ import java.util.ArrayList class AccountsActivity : AppCompatActivity() { internal lateinit var alAdapter: AccountListAdapter - internal var aor = "" + internal var password = "" public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -73,8 +78,25 @@ class AccountsActivity : AppCompatActivity() { } } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.accounts_menu, menu) + return true + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { + R.id.export_accounts -> { + if (Utils.requestPermission(this, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) + askPassword("Encrypt Password") + } + R.id.import_accounts -> { + if (Utils.requestPermission(this, + android.Manifest.permission.READ_EXTERNAL_STORAGE)) { + askPassword("Decrypt Password") + + } + } android.R.id.home -> { Log.d("Baresip", "Back array was pressed at Accounts") val i = Intent() @@ -85,6 +107,45 @@ class AccountsActivity : AppCompatActivity() { return true } + private fun askPassword(title: String) { + val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + val builder = AlertDialog.Builder(this) + builder.setTitle(title) + val viewInflated = LayoutInflater.from(this) + .inflate(R.layout.password_dialog, findViewById(android.R.id.content) as ViewGroup, + false) + val input = viewInflated.findViewById(R.id.password) as EditText + builder.setView(viewInflated) + builder.setPositiveButton(android.R.string.ok) { dialog, _ -> + dialog.dismiss() + password = input.text.toString() + if (password != "") { + if (title == "Encrypt Password") { + if (exportAccounts(dir, password)) + Utils.alertView(this, "Info", + "Exported accounts to Download folder as 'accounts.bs'") + else + Utils.alertView(this, "Error", + "Failed to export accounts to Download folder.") + } else { + if (importAccounts(dir, password)) + Utils.alertView(this, "Info", + "Imported accounts from Download folder " + + "'accounts.bs'. You need to restart baresip.") + else + Utils.alertView(this, "Error", + "Failed to import accounts from Download folder. " + + "Either file 'accounts.bs' doesn't exist or " + + "you gave wrong Decrypt Password.") + } + } + } + builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> + dialog.cancel() + } + builder.show() + } + companion object { internal var filesPath = "" @@ -104,6 +165,22 @@ class AccountsActivity : AppCompatActivity() { Utils.putFileContents(File(filesPath + "/accounts"), accounts) // Log.d("Baresip", "Saved accounts '${accounts}' to '${filesPath}/accounts'") } + + fun exportAccounts(path: File, password: String): Boolean { + var accounts = "" + for (a in Account.accounts()) + accounts = accounts + a.print() + "\n" + return Utils.putFileContents(File(path, "accounts.bs"), + Utils.encrypt(accounts, password)) + } + + fun importAccounts(path: File, password: String): Boolean { + val content = Utils.getFileContents(File(path, "accounts.bs")) + if (content == "Failed") return false + val accounts = Utils.decrypt(content, password) + if (accounts == "") return false + return Utils.putFileContents(File(filesPath + "/accounts"), accounts) + } } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt index ff04e530..cff14b45 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt @@ -2,7 +2,7 @@ package com.tutpro.baresip import android.app.Activity import android.content.* -import android.os.Bundle +import android.os.Bundle import android.os.Environment import android.support.v7.app.AppCompatActivity import android.view.Menu @@ -59,7 +59,7 @@ class ContactsActivity : AppCompatActivity() { R.id.export_contacts -> { if (saveContacts(dir)) Utils.alertView(this,"", - "Exported contacts to Download folder.") + "Exported contacts to Download folder as 'contacts.bs'.") else Utils.alertView(this,"Error", "Failed to export contacts to Download folder. " + @@ -100,11 +100,11 @@ class ContactsActivity : AppCompatActivity() { var contents = "" for (c in Contact.contacts()) contents += "\"${c.name}\" ${c.uri}\n" - return Utils.putFileContents(File(path, "contacts"), contents) + return Utils.putFileContents(File(path, "contacts.bs"), contents) } fun restoreContacts(path: File): Boolean { - val content = Utils.getFileContents(File(path, "contacts")) + val content = Utils.getFileContents(File(path, "contacts.bs")) if (content == "Failed") return false Api.contacts_remove() Contact.contacts().clear() diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index 8b79ab44..4dc94703 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -9,8 +9,6 @@ import android.support.v4.content.ContextCompat import android.support.v7.app.AlertDialog import android.support.v7.app.AppCompatActivity import android.os.Bundle -import android.view.Menu -import android.view.MenuItem import android.content.pm.PackageManager import android.media.AudioManager import android.os.Build @@ -19,7 +17,6 @@ import android.support.v4.content.LocalBroadcastManager import android.view.inputmethod.InputMethodManager import android.text.InputType import android.text.TextWatcher -import android.widget.RelativeLayout import android.widget.* import android.view.* @@ -130,7 +127,7 @@ class MainActivity : AppCompatActivity() { Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { Log.w("Baresip", "Baresip does not have RECORD_AUDIO permission") ActivityCompat.requestPermissions(this, - arrayOf(Manifest.permission.RECORD_AUDIO), RECORD_AUDIO_PERMISSION) + arrayOf(Manifest.permission.RECORD_AUDIO), PERMISSION_REQUEST_CODE) } aorSpinner = findViewById(R.id.AoRList) @@ -1142,7 +1139,8 @@ class MainActivity : AppCompatActivity() { const val CONTACT_CODE = 7 const val MESSAGES_CODE = 8 const val MESSAGE_CODE = 9 - const val RECORD_AUDIO_PERMISSION = 1 + + const val PERMISSION_REQUEST_CODE = 1 } diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index 2f55bf68..5eddd9a1 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -1,16 +1,32 @@ package com.tutpro.baresip +import android.app.Activity import android.app.ActivityManager import android.content.Context import android.support.v7.app.AlertDialog import android.os.PowerManager import android.app.KeyguardManager import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle +import android.support.v4.app.ActivityCompat +import android.support.v4.content.ContextCompat import android.text.Editable import android.text.TextWatcher +import android.util.Base64 +import android.util.Base64.encodeToString + +import kotlin.collections.ArrayList +import kotlin.Exception import java.io.* +import java.security.SecureRandom +import java.util.* +import java.nio.ByteBuffer +import javax.crypto.Cipher +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.PBEKeySpec object Utils { @@ -285,6 +301,75 @@ object Utils { } } + fun requestPermission(ctx: Context, permission: String) : Boolean { + if (ContextCompat.checkSelfPermission(ctx, permission) != PackageManager.PERMISSION_GRANTED) { + Log.w("Baresip", "Baresip does not have $permission permission") + ActivityCompat.requestPermissions(ctx as Activity, arrayOf(permission), + MainActivity.PERMISSION_REQUEST_CODE) + return false + } + return true + } + + fun encrypt(plainText: String, password: String): String { + val sr = SecureRandom.getInstance("SHA1PRNG") + val salt = ByteArray(16) + sr.nextBytes(salt) + val iterationCount = Random().nextInt(1024) + 512 + val spec = PBEKeySpec(password.toCharArray(), salt, iterationCount, 256) + val secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(spec) + val iv = ByteArray(12) + SecureRandom().nextBytes(iv) + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + val parameterSpec = GCMParameterSpec(128, iv) + cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec) + val cipherText = cipher.doFinal(plainText.toByteArray()) + val byteBuffer = ByteBuffer.allocate(Int.SIZE_BYTES + salt.size + Int.SIZE_BYTES + + Int.SIZE_BYTES + iv.size + cipherText.size) + byteBuffer.putInt(salt.size) + byteBuffer.put(salt) + byteBuffer.putInt(iterationCount) + byteBuffer.putInt(iv.size) + byteBuffer.put(iv) + byteBuffer.put(cipherText) + return encodeToString(byteBuffer.array(), Base64.DEFAULT) + } + + fun decrypt(cipherMessage: String, password: String): String { + val byteBuffer = ByteBuffer.wrap(Base64.decode(cipherMessage, Base64.DEFAULT)) + val saltLength = byteBuffer.getInt() + if (saltLength != 16) { + Log.w("Baresip", "invalid salt length $saltLength") + return "" + } + val salt = ByteArray(saltLength) + byteBuffer.get(salt) + val iterationCount = byteBuffer.getInt() + if ((iterationCount < 512) || (iterationCount > 1023 + 512)) { + Log.w("Baresip", "invalid iteratorCount $iterationCount") + return "" + } + val spec = PBEKeySpec(password.toCharArray(), salt, iterationCount, 256) + val secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(spec) + val ivLength = byteBuffer.getInt() + if (ivLength != 12) { + Log.d("Baresip", "invalid iv length $ivLength") + return "" + } + val iv = ByteArray(ivLength) + byteBuffer.get(iv) + val cipherText = ByteArray(byteBuffer.remaining()) + byteBuffer.get(cipherText) + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv)) + try { + return String(cipher.doFinal(cipherText)) + } catch (e: Exception) { + Log.w("Baresip", "Decryption failed ${e.printStackTrace()}") + return "" + } + } + fun dumpIntent(intent: Intent) { val bundle: Bundle = intent.extras ?: return diff --git a/app/src/main/res/layout/password_dialog.xml b/app/src/main/res/layout/password_dialog.xml new file mode 100644 index 00000000..f971b727 --- /dev/null +++ b/app/src/main/res/layout/password_dialog.xml @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/accounts_menu.xml b/app/src/main/res/menu/accounts_menu.xml new file mode 100644 index 00000000..2fa8d185 --- /dev/null +++ b/app/src/main/res/menu/accounts_menu.xml @@ -0,0 +1,9 @@ + + + + + + +