- Added possibility to export and import accounts.
This commit is contained in:
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
20
app/src/main/res/layout/password_dialog.xml
Normal file
20
app/src/main/res/layout/password_dialog.xml
Normal file
@ -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
app/src/main/res/menu/accounts_menu.xml
Normal file
9
app/src/main/res/menu/accounts_menu.xml
Normal 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>
|
||||
Reference in New Issue
Block a user