diff --git a/app/build.gradle b/app/build.gradle index e60da3a..6619da6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt index 6f1d3a9..5df194e 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt @@ -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. */ diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ImportBrainkeyActivity.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ImportBrainkeyActivity.kt index bf524cc..201663e 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ImportBrainkeyActivity.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ImportBrainkeyActivity.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/BitsyDatabase.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/BitsyDatabase.kt index 055cd8f..59d5e75 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/BitsyDatabase.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/BitsyDatabase.kt @@ -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 - } } } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/BrainKeyDao.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/BrainKeyDao.kt deleted file mode 100644 index f6e4326..0000000 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/BrainKeyDao.kt +++ /dev/null @@ -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> -} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/UserAccountAuthorityDao.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/UserAccountAuthorityDao.kt deleted file mode 100644 index 0bfd786..0000000 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/daos/UserAccountAuthorityDao.kt +++ /dev/null @@ -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> -} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/Authority.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/Authority.kt index 3f018d5..8afe6f1 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/Authority.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/Authority.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/BrainKey.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/BrainKey.kt deleted file mode 100644 index 3d77398..0000000 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/BrainKey.kt +++ /dev/null @@ -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 -) \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/UserAccount.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/UserAccount.kt index 9460ea7..06560b7 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/UserAccount.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/UserAccount.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/UserAccountAuthority.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/UserAccountAuthority.kt deleted file mode 100644 index 99db269..0000000 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/UserAccountAuthority.kt +++ /dev/null @@ -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 -) \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AuthorityRepository.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AuthorityRepository.kt new file mode 100644 index 0000000..6a2ff4f --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AuthorityRepository.kt @@ -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() { + + override fun doInBackground(vararg authorities: Authority): Void? { + mAsyncTaskDao.insert(authorities[0]) + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/UserAccountRepository.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/UserAccountRepository.kt new file mode 100644 index 0000000..e11963f --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/UserAccountRepository.kt @@ -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() { + + override fun doInBackground(vararg userAccounts: UserAccount): Void? { + mAsyncTaskDao.insert(userAccounts[0]) + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt index d2128c5..36fd11a 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt @@ -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" diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/CryptoUtils.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/CryptoUtils.kt new file mode 100644 index 0000000..7fc8757 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/CryptoUtils.kt @@ -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) + } +} +