- 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"
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 'me.dm7.barcodescanner:zxing:1.9.8'
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.Handler
import android.os.IBinder
import android.os.PersistableBundle
import android.util.Log
import android.widget.Toast
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() {
super.onResume()
@ -99,6 +103,16 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
// .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.
*/

View file

@ -1,19 +1,21 @@
package cy.agorise.bitsybitshareswallet.activities
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.preference.PreferenceManager
import android.util.Log
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.Toast
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.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.graphenej.Address
import cy.agorise.graphenej.BrainKey
import cy.agorise.graphenej.UserAccount
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
import cy.agorise.graphenej.*
import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.calls.GetAccounts
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 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() {
private val TAG = "ImportBrainkeyActivity"
@ -179,17 +184,19 @@ class ImportBrainkeyActivity : ConnectedActivity() {
MaterialDialog(this)
.title(R.string.dialog__account_candidates_title)
.message(R.string.dialog__account_candidates_content)
.listItems(items = candidates) { dialog, index, _ ->
.listItemsSingleChoice (items = candidates, initialSelection = -1) { _, index, _ ->
if (index >= 0) {
// If one account was selected, we keep a reference to it and
// store the account properties
// TODO make sure this is reached
mUserAccount = mUserAccountCandidates!![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()
} else if (accountPropertiesList.size == 1) {
onAccountSelected(accountPropertiesList[0])
@ -216,25 +223,76 @@ class ImportBrainkeyActivity : ConnectedActivity() {
private fun onAccountSelected(accountProperties: AccountProperties) {
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
// 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
// PreferenceManager.getDefaultSharedPreferences(applicationContext)
// .edit()
// .putString(Constants.KEY_CURRENT_ACCOUNT_ID, mUserAccount!!.objectId)
// .apply()
PreferenceManager.getDefaultSharedPreferences(this)
.edit()
.putString(Constants.KEY_CURRENT_ACCOUNT_ID, mUserAccount!!.objectId)
.apply()
// Trying to store all possible authorities (owner, active and memo)
// for (i in 0..2) {
// mBrainKey.setSequenceNumber(i)
// saveAccountAuthorities(mBrainKey, accountProperties)
// }
// Trying to store all possible authorities (owner, active and memo) into the database
val ownerAuthority = accountProperties.owner
val activeAuthority = accountProperties.active
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,
Authority::class,
Balance::class,
BrainKey::class,
EquivalentValue::class,
Operation::class,
Transfer::class,
UserAccount::class,
UserAccountAuthority::class
UserAccount::class
], version = 1, exportSchema = false)
abstract class BitsyDatabase : RoomDatabase() {
abstract fun assetDao(): AssetDao
abstract fun authorityDao(): AuthorityDao
abstract fun balanceDao(): BalanceDao
abstract fun brainKeyDao(): BrainKeyDao
abstract fun equivalentValueDao(): EquivalentValueDao
abstract fun operationDao(): OperationDao
abstract fun transferDao(): TransferDao
abstract fun userAccountDao(): UserAccountDao
abstract fun userAccountAuthorityDao(): UserAccountAuthorityDao
companion object {
@ -48,9 +44,5 @@ abstract class BitsyDatabase : RoomDatabase() {
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 (
@PrimaryKey(autoGenerate = true)
@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
@ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "is_ltm") val isLtm: Boolean,
@ColumnInfo(name = "weight_threshold") val weightThreshold: Int
@ColumnInfo(name = "is_ltm") val isLtm: Boolean
)

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 */
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 */
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)
}
}