- Added possibility to export and import accounts.

This commit is contained in:
Juha Heinanen
2019-04-16 15:30:43 +03:00
parent 4afe9ca23c
commit 4899f31c6a
6 changed files with 199 additions and 10 deletions
@@ -3,9 +3,14 @@ package com.tutpro.baresip
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.* import android.widget.*
import android.app.AlertDialog
import android.view.ViewGroup
import android.view.LayoutInflater
import java.io.File import java.io.File
import java.util.ArrayList import java.util.ArrayList
@@ -13,8 +18,8 @@ import java.util.ArrayList
class AccountsActivity : AppCompatActivity() { class AccountsActivity : AppCompatActivity() {
internal lateinit var alAdapter: AccountListAdapter internal lateinit var alAdapter: AccountListAdapter
internal var aor = "" internal var aor = ""
internal var password = ""
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { 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 -> { android.R.id.home -> {
Log.d("Baresip", "Back array was pressed at Accounts") Log.d("Baresip", "Back array was pressed at Accounts")
val i = Intent() val i = Intent()
@@ -85,6 +107,45 @@ class AccountsActivity : AppCompatActivity() {
return true 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 { companion object {
internal var filesPath = "" internal var filesPath = ""
@@ -104,6 +165,22 @@ class AccountsActivity : AppCompatActivity() {
Utils.putFileContents(File(filesPath + "/accounts"), accounts) Utils.putFileContents(File(filesPath + "/accounts"), accounts)
// Log.d("Baresip", "Saved accounts '${accounts}' to '${filesPath}/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)
}
} }
} }
@@ -59,7 +59,7 @@ class ContactsActivity : AppCompatActivity() {
R.id.export_contacts -> { R.id.export_contacts -> {
if (saveContacts(dir)) if (saveContacts(dir))
Utils.alertView(this,"", Utils.alertView(this,"",
"Exported contacts to Download folder.") "Exported contacts to Download folder as 'contacts.bs'.")
else else
Utils.alertView(this,"Error", Utils.alertView(this,"Error",
"Failed to export contacts to Download folder. " + "Failed to export contacts to Download folder. " +
@@ -100,11 +100,11 @@ class ContactsActivity : AppCompatActivity() {
var contents = "" var contents = ""
for (c in Contact.contacts()) for (c in Contact.contacts())
contents += "\"${c.name}\" ${c.uri}\n" 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 { 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 if (content == "Failed") return false
Api.contacts_remove() Api.contacts_remove()
Contact.contacts().clear() Contact.contacts().clear()
@@ -9,8 +9,6 @@ import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
@@ -19,7 +17,6 @@ import android.support.v4.content.LocalBroadcastManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.text.InputType import android.text.InputType
import android.text.TextWatcher import android.text.TextWatcher
import android.widget.RelativeLayout
import android.widget.* import android.widget.*
import android.view.* import android.view.*
@@ -130,7 +127,7 @@ class MainActivity : AppCompatActivity() {
Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
Log.w("Baresip", "Baresip does not have RECORD_AUDIO permission") Log.w("Baresip", "Baresip does not have RECORD_AUDIO permission")
ActivityCompat.requestPermissions(this, ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.RECORD_AUDIO), RECORD_AUDIO_PERMISSION) arrayOf(Manifest.permission.RECORD_AUDIO), PERMISSION_REQUEST_CODE)
} }
aorSpinner = findViewById(R.id.AoRList) aorSpinner = findViewById(R.id.AoRList)
@@ -1142,7 +1139,8 @@ class MainActivity : AppCompatActivity() {
const val CONTACT_CODE = 7 const val CONTACT_CODE = 7
const val MESSAGES_CODE = 8 const val MESSAGES_CODE = 8
const val MESSAGE_CODE = 9 const val MESSAGE_CODE = 9
const val RECORD_AUDIO_PERMISSION = 1
const val PERMISSION_REQUEST_CODE = 1
} }
@@ -1,16 +1,32 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.os.PowerManager import android.os.PowerManager
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.text.Editable import android.text.Editable
import android.text.TextWatcher 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.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 { 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) { fun dumpIntent(intent: Intent) {
val bundle: Bundle = intent.extras ?: return val bundle: Bundle = intent.extras ?: return
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp" >
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<AutoCompleteTextView
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:imeOptions="actionDone"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
</FrameLayout>
+9
View File
@@ -0,0 +1,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/export_accounts"
android:title="Export" />
<item android:id="@+id/import_accounts"
android:title="Import" />
</menu>