- Simplified Database structure to include only what is going to be used for the wallet.

- Properly distroy NetworkService connection when ConnectedActivty is distroyed to avoid crashes.
- Created UserAccountRepository and AuthorityRepository to handle their corresponding database operations in an asynchronous way, because Room enforces database operations to be done in a thread other than the UI thread.
- In ImportBrainkeyActivity when a proper brainkey is given save the imported account into the database, put a flag that tells the app there is a current account imported and send the user to MainActivity.
This commit is contained in:
Severiano Jaramillo 2018-11-24 09:11:57 -06:00
parent 847e8a8d7f
commit 175d48f8c6
14 changed files with 244 additions and 108 deletions

View file

@ -47,6 +47,7 @@ dependencies {
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
implementation 'org.bitcoinj:bitcoinj-core:0.14.3' implementation 'org.bitcoinj:bitcoinj-core:0.14.3'
implementation 'com.moldedbits.r2d2:r2d2:1.0.1'
implementation 'com.google.zxing:core:3.3.1' implementation 'com.google.zxing:core:3.3.1'
implementation 'me.dm7.barcodescanner:zxing:1.9.8' implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation 'com.afollestad.material-dialogs:core:2.0.0-rc1' implementation 'com.afollestad.material-dialogs:core:2.0.0-rc1'

View file

@ -7,7 +7,6 @@ import android.content.ServiceConnection
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.PersistableBundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -84,6 +83,11 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
} }
} }
override fun onDestroy() {
super.onDestroy()
if (!mDisposable!!.isDisposed) mDisposable!!.dispose()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -99,6 +103,16 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
// .getLong(Constants.KEY_ACCOUNT_OPERATION_COUNT, -1) // .getLong(Constants.KEY_ACCOUNT_OPERATION_COUNT, -1)
} }
override fun onPause() {
super.onPause()
// Unbinding from network service
if (mShouldUnbindNetwork) {
unbindService(this)
mShouldUnbindNetwork = false
}
// mHandler.removeCallbacks(mCheckMissingPaymentsTask)
}
/** /**
* Task used to perform a redundant payment check. * Task used to perform a redundant payment check.
*/ */

View file

@ -1,19 +1,21 @@
package cy.agorise.bitsybitshareswallet.activities package cy.agorise.bitsybitshareswallet.activities
import android.content.DialogInterface import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.util.Log import android.util.Log
import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.Toast import android.widget.Toast
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItemsSingleChoice
import cy.agorise.bitsybitshareswallet.R import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
import cy.agorise.bitsybitshareswallet.models.Authority
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository
import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.graphenej.Address import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
import cy.agorise.graphenej.BrainKey import cy.agorise.graphenej.*
import cy.agorise.graphenej.UserAccount
import cy.agorise.graphenej.api.ConnectionStatusUpdate import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.calls.GetAccounts import cy.agorise.graphenej.api.calls.GetAccounts
import cy.agorise.graphenej.api.calls.GetKeyReferences import cy.agorise.graphenej.api.calls.GetKeyReferences
@ -23,6 +25,9 @@ import kotlinx.android.synthetic.main.activity_import_brainkey.*
import org.bitcoinj.core.ECKey import org.bitcoinj.core.ECKey
import java.util.ArrayList import java.util.ArrayList
// TODO Add method to load the 20? most important assets
// TODO add progress bar or something while the user waits for the import response from the node
class ImportBrainkeyActivity : ConnectedActivity() { class ImportBrainkeyActivity : ConnectedActivity() {
private val TAG = "ImportBrainkeyActivity" private val TAG = "ImportBrainkeyActivity"
@ -179,17 +184,19 @@ class ImportBrainkeyActivity : ConnectedActivity() {
MaterialDialog(this) MaterialDialog(this)
.title(R.string.dialog__account_candidates_title) .title(R.string.dialog__account_candidates_title)
.message(R.string.dialog__account_candidates_content) .message(R.string.dialog__account_candidates_content)
.listItems(items = candidates) { dialog, index, _ -> .listItemsSingleChoice (items = candidates, initialSelection = -1) { _, index, _ ->
if (index >= 0) { if (index >= 0) {
// If one account was selected, we keep a reference to it and // If one account was selected, we keep a reference to it and
// store the account properties // store the account properties
// TODO make sure this is reached
mUserAccount = mUserAccountCandidates!![index] mUserAccount = mUserAccountCandidates!![index]
onAccountSelected(accountPropertiesList[index]) onAccountSelected(accountPropertiesList[index])
dialog.dismiss()
} }
} }
.negativeButton(android.R.string.cancel) { mKeyReferencesAttempts = 0 } .positiveButton(android.R.string.ok)
.negativeButton(android.R.string.cancel) {
mKeyReferencesAttempts = 0
}
.cancelable(false)
.show() .show()
} else if (accountPropertiesList.size == 1) { } else if (accountPropertiesList.size == 1) {
onAccountSelected(accountPropertiesList[0]) onAccountSelected(accountPropertiesList[0])
@ -216,25 +223,76 @@ class ImportBrainkeyActivity : ConnectedActivity() {
private fun onAccountSelected(accountProperties: AccountProperties) { private fun onAccountSelected(accountProperties: AccountProperties) {
mUserAccount!!.name = accountProperties.name mUserAccount!!.name = accountProperties.name
Toast.makeText(this, "Account: "+accountProperties.name, Toast.LENGTH_SHORT).show() val encryptedPIN = CryptoUtils.encrypt(this, tietPin.text!!.toString())
val password = tietPin.text!!.toString() // Stores the user selected PIN encrypted
PreferenceManager.getDefaultSharedPreferences(this)
.edit()
.putString(Constants.KEY_ENCRYPTED_PIN, encryptedPIN)
.apply()
// Stores the accounts this key refers to // Stores the accounts this key refers to
// database.putOwnedUserAccounts(applicationContext, mUserAccount, password) val id = accountProperties.id
val name = accountProperties.name
val isLTM = accountProperties.membership_expiration_date == Constants.LIFETIME_EXPIRATION_DATE
val userAccount = cy.agorise.bitsybitshareswallet.models.UserAccount(id, name, isLTM)
val userAccountRepository = UserAccountRepository(application)
userAccountRepository.insert(userAccount)
// Stores the id of the currently active user account // Stores the id of the currently active user account
// PreferenceManager.getDefaultSharedPreferences(applicationContext) PreferenceManager.getDefaultSharedPreferences(this)
// .edit() .edit()
// .putString(Constants.KEY_CURRENT_ACCOUNT_ID, mUserAccount!!.objectId) .putString(Constants.KEY_CURRENT_ACCOUNT_ID, mUserAccount!!.objectId)
// .apply() .apply()
// Trying to store all possible authorities (owner, active and memo) // Trying to store all possible authorities (owner, active and memo) into the database
// for (i in 0..2) { val ownerAuthority = accountProperties.owner
// mBrainKey.setSequenceNumber(i) val activeAuthority = accountProperties.active
// saveAccountAuthorities(mBrainKey, accountProperties) val options = accountProperties.options
// }
// TODO move to MainActivity for (i in 0..2) {
mBrainKey!!.sequenceNumber = i
val publicKey = PublicKey(ECKey.fromPublicOnly(mBrainKey!!.privateKey.pubKey))
if (ownerAuthority.keyAuths.keys.contains(publicKey)) {
addAuthorityToDatabase(accountProperties.id, AuthorityType.OWNER.ordinal, mBrainKey!!)
}
if (activeAuthority.keyAuths.keys.contains(publicKey)) {
addAuthorityToDatabase(accountProperties.id, AuthorityType.ACTIVE.ordinal, mBrainKey!!)
}
if (options.memoKey == publicKey) {
addAuthorityToDatabase(accountProperties.id, AuthorityType.MEMO.ordinal, mBrainKey!!)
}
}
// Stores a flag into the SharedPreferences to tell the app there is an active account and there is no need
// to show this activity again, until the account is removed.
PreferenceManager.getDefaultSharedPreferences(this)
.edit()
.putBoolean(Constants.KEY_INITIAL_SETUP_DONE, true)
.apply()
// Send the user to the MainActivity
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
/**
* Adds the given BrainKey encrypted as AuthorityType of userId.
*/
private fun addAuthorityToDatabase(userId: String, authorityType: Int, brainKey: BrainKey) {
val brainKeyWords = brainKey.brainKey
val sequenceNumber = brainKey.sequenceNumber
val encryptedBrainKey = CryptoUtils.encrypt(this, brainKeyWords)
val encryptedSequenceNumber = CryptoUtils.encrypt(this, sequenceNumber.toString())
val authority = Authority(0, userId, authorityType, encryptedBrainKey, encryptedSequenceNumber)
val authorityRepository = AuthorityRepository(application)
authorityRepository.insert(authority)
} }
} }

View file

@ -10,24 +10,20 @@ import cy.agorise.bitsybitshareswallet.models.*
Asset::class, Asset::class,
Authority::class, Authority::class,
Balance::class, Balance::class,
BrainKey::class,
EquivalentValue::class, EquivalentValue::class,
Operation::class, Operation::class,
Transfer::class, Transfer::class,
UserAccount::class, UserAccount::class
UserAccountAuthority::class
], version = 1, exportSchema = false) ], version = 1, exportSchema = false)
abstract class BitsyDatabase : RoomDatabase() { abstract class BitsyDatabase : RoomDatabase() {
abstract fun assetDao(): AssetDao abstract fun assetDao(): AssetDao
abstract fun authorityDao(): AuthorityDao abstract fun authorityDao(): AuthorityDao
abstract fun balanceDao(): BalanceDao abstract fun balanceDao(): BalanceDao
abstract fun brainKeyDao(): BrainKeyDao
abstract fun equivalentValueDao(): EquivalentValueDao abstract fun equivalentValueDao(): EquivalentValueDao
abstract fun operationDao(): OperationDao abstract fun operationDao(): OperationDao
abstract fun transferDao(): TransferDao abstract fun transferDao(): TransferDao
abstract fun userAccountDao(): UserAccountDao abstract fun userAccountDao(): UserAccountDao
abstract fun userAccountAuthorityDao(): UserAccountAuthorityDao
companion object { companion object {
@ -48,9 +44,5 @@ abstract class BitsyDatabase : RoomDatabase() {
return INSTANCE return INSTANCE
} }
fun destroyInstance() {
INSTANCE = null
}
} }
} }

View file

@ -1,16 +0,0 @@
package cy.agorise.bitsybitshareswallet.daos
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import cy.agorise.bitsybitshareswallet.models.BrainKey
@Dao
interface BrainKeyDao {
@Insert
fun insert(brainKey: BrainKey)
@Query("SELECT * FROM brain_keys")
fun getAllBrainKeys(): LiveData<List<BrainKey>>
}

View file

@ -1,17 +0,0 @@
package cy.agorise.bitsybitshareswallet.daos
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import cy.agorise.bitsybitshareswallet.models.Authority
import cy.agorise.bitsybitshareswallet.models.UserAccountAuthority
@Dao
interface UserAccountAuthorityDao {
@Insert
fun insert(userAccountAuthority: UserAccountAuthority)
// @Query("SELECT * FROM authorities INNER JOIN user_accounts__authorities ON user_accounts.id=user_accounts__authorities.user_account_id WHERE user_accounts__authorities.user_account_id=:userAccountId")
// fun getAuthoritiesForUserAccount(userAccountId: String): LiveData<List<Authority>>
}

View file

@ -8,6 +8,8 @@ import androidx.room.PrimaryKey
data class Authority ( data class Authority (
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") val id: Long, @ColumnInfo(name = "id") val id: Long,
@ColumnInfo(name = "encrypted_private_key") val encryptedBrainkey: String, @ColumnInfo(name = "user_id") val userId: String,
@ColumnInfo(name = "user_id") val userId: String @ColumnInfo(name = "authority_type") val authorityType: Int,
@ColumnInfo(name = "encrypted_brain_key") val encryptedBrainKey: String,
@ColumnInfo(name = "encrypted_sequence_number") val encryptedSequenceNumber: String
) )

View file

@ -1,13 +0,0 @@
package cy.agorise.bitsybitshareswallet.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName="brain_keys")
data class BrainKey(
@PrimaryKey
@ColumnInfo(name = "public_key") val publicKey: String,
@ColumnInfo(name = "encrypted_brain_key") val encryptedBrainKey: String,
@ColumnInfo(name = "sequence_number") val sequenceNumber: Long
)

View file

@ -9,6 +9,5 @@ data class UserAccount (
@PrimaryKey @PrimaryKey
@ColumnInfo(name = "id") val id: String, @ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "is_ltm") val isLtm: Boolean, @ColumnInfo(name = "is_ltm") val isLtm: Boolean
@ColumnInfo(name = "weight_threshold") val weightThreshold: Int
) )

View file

@ -1,25 +0,0 @@
package cy.agorise.bitsybitshareswallet.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
/**
* Table to create a N:N relationship between [UserAccount] and [Authority]
*/
@Entity(tableName = "user_accounts__authorities",
primaryKeys = ["user_account_id", "authority_id"],
foreignKeys = [ForeignKey(
entity = UserAccount::class,
parentColumns = ["id"],
childColumns = ["user_account_id"]
), ForeignKey(
entity = Authority::class,
parentColumns = ["id"],
childColumns = ["authority_id"]
)])
data class UserAccountAuthority (
@ColumnInfo(name = "user_account_id") val userAccountId: String,
@ColumnInfo(name = "authority_id") val authorityId: Long,
@ColumnInfo(name = "weight") val weight: Int
)

View file

@ -0,0 +1,30 @@
package cy.agorise.bitsybitshareswallet.repositories
import android.app.Application
import android.os.AsyncTask
import cy.agorise.bitsybitshareswallet.daos.AuthorityDao
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
import cy.agorise.bitsybitshareswallet.models.Authority
class AuthorityRepository internal constructor(application: Application) {
private val mAuthorityDao: AuthorityDao
init {
val db = BitsyDatabase.getDatabase(application)
mAuthorityDao = db!!.authorityDao()
}
fun insert(authority: Authority) {
insertAsyncTask(mAuthorityDao).execute(authority)
}
private class insertAsyncTask internal constructor(private val mAsyncTaskDao: AuthorityDao) :
AsyncTask<Authority, Void, Void>() {
override fun doInBackground(vararg authorities: Authority): Void? {
mAsyncTaskDao.insert(authorities[0])
return null
}
}
}

View file

@ -0,0 +1,30 @@
package cy.agorise.bitsybitshareswallet.repositories
import android.app.Application
import android.os.AsyncTask
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
import cy.agorise.bitsybitshareswallet.daos.UserAccountDao
import cy.agorise.bitsybitshareswallet.models.UserAccount
class UserAccountRepository internal constructor(application: Application) {
private val mUserAccountDao: UserAccountDao
init {
val db = BitsyDatabase.getDatabase(application)
mUserAccountDao = db!!.userAccountDao()
}
fun insert(userAccount: UserAccount) {
insertAsyncTask(mUserAccountDao).execute(userAccount)
}
private class insertAsyncTask internal constructor(private val mAsyncTaskDao: UserAccountDao) :
AsyncTask<UserAccount, Void, Void>() {
override fun doInBackground(vararg userAccounts: UserAccount): Void? {
mAsyncTaskDao.insert(userAccounts[0])
return null
}
}
}

View file

@ -15,6 +15,15 @@ object Constants {
/** The minimum required length for a PIN number */ /** The minimum required length for a PIN number */
const val MIN_PIN_LENGTH = 6 const val MIN_PIN_LENGTH = 6
/** The user selected encrypted PIN */
const val KEY_ENCRYPTED_PIN = "key_encrypted_pin"
/**
* LTM accounts come with an expiration date expressed as this string.
* This is used to recognize such accounts from regular ones.
*/
const val LIFETIME_EXPIRATION_DATE = "1969-12-31T23:59:59"
/** Key used to store if the initial setup is already done or not */ /** Key used to store if the initial setup is already done or not */
const val KEY_INITIAL_SETUP_DONE = "key_initial_setup_done" const val KEY_INITIAL_SETUP_DONE = "key_initial_setup_done"

View file

@ -0,0 +1,72 @@
package cy.agorise.bitsybitshareswallet.utils
import android.content.Context
import android.preference.PreferenceManager
import com.moldedbits.r2d2.R2d2
import javax.crypto.AEADBadTagException
/**
* Class that provides encryption/decryption support by using the key management framework provided
* by the KeyStore system.
*
* The implemented scheme was taken from [this](https://medium.com/@ericfu/securely-storing-secrets-in-an-android-application-501f030ae5a3)> blog post.
*
* @see [Android Keystore System](https://developer.android.com/training/articles/keystore.html)
*/
object CryptoUtils {
/**
* Encrypts and stores a key-value pair in the shared preferences
* @param context The application context
* @param key The key to be used to reference the data
* @param value The actual value to be stored
*/
fun put(context: Context, key: String, value: String) {
val r2d2 = R2d2(context)
val encrypted = r2d2.encryptData(value)
PreferenceManager
.getDefaultSharedPreferences(context)
.edit()
.putString(key, encrypted)
.apply()
}
/**
* Retrieves and decrypts an encrypted value from the shared preferences
* @param context The application context
* @param key The key used to reference the data
* @return The plaintext version of the encrypted data
*/
operator fun get(context: Context, key: String): String {
val r2d2 = R2d2(context)
val encrypted = PreferenceManager.getDefaultSharedPreferences(context).getString(key, null)
return r2d2.decryptData(encrypted)
}
/**
* Encrypts some data
* @param context The application context
* @param plaintext The plaintext version of the data
* @return Encrypted data
*/
fun encrypt(context: Context, plaintext: String): String {
val r2d2 = R2d2(context)
return r2d2.encryptData(plaintext)
}
/**
* Decrypts some data
* @param context The application context
* @param ciphertext The ciphertext version of the data
* @return Decrypted data
*/
@Throws(AEADBadTagException::class)
fun decrypt(context: Context, ciphertext: String): String {
val r2d2 = R2d2(context)
return r2d2.decryptData(ciphertext)
}
}