diff --git a/CHANGELOG.md b/CHANGELOG.md index bb951348..4c322898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [Unreleased] + +### Added + +- Ability to have both file and seed phrase wallets in the app and switch between them + +### Removed + +- Ability to create new accounts and identities in a file wallet. + We recommend that you migrate to a seed phrase wallet + in order to make use of the full range of CryptoX features. + ## [1.4.0] - 2024-12-16 ### Added diff --git a/app/build.gradle b/app/build.gradle index c86a790f..226be8fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -285,7 +285,7 @@ dependencies { implementation 'androidx.browser:browser:1.4.0' implementation 'com.github.Redman1037:TSnackBar:V2.0.0' - implementation 'com.github.Dimezis:BlurView:version-2.0.3' + implementation 'com.github.Dimezis:BlurView:version-2.0.6' // OkHttp/Retrofit implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1' diff --git a/app/schemas/com.concordium.wallet.data.room.WalletDatabase/10.json b/app/schemas/com.concordium.wallet.data.room.WalletDatabase/10.json new file mode 100644 index 00000000..556abb5b --- /dev/null +++ b/app/schemas/com.concordium.wallet.data.room.WalletDatabase/10.json @@ -0,0 +1,499 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "8e6839ae303bff1c2c78e11537326637", + "entities": [ + { + "tableName": "identity_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `status` TEXT NOT NULL, `detail` TEXT, `code_uri` TEXT NOT NULL, `next_account_number` INTEGER NOT NULL, `identity_provider` TEXT NOT NULL, `identity_object` TEXT, `private_id_object_data_encrypted` TEXT, `identity_provider_id` INTEGER NOT NULL, `identity_index` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "detail", + "columnName": "detail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "codeUri", + "columnName": "code_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nextAccountNumber", + "columnName": "next_account_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identityProvider", + "columnName": "identity_provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "identityObject", + "columnName": "identity_object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "privateIdObjectDataEncrypted", + "columnName": "private_id_object_data_encrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "identityProviderId", + "columnName": "identity_provider_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identityIndex", + "columnName": "identity_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_identity_table_identity_provider_id_identity_index", + "unique": true, + "columnNames": [ + "identity_provider_id", + "identity_index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_identity_table_identity_provider_id_identity_index` ON `${TABLE_NAME}` (`identity_provider_id`, `identity_index`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "account_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `identity_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `submission_id` TEXT NOT NULL, `transaction_status` INTEGER NOT NULL, `encrypted_account_data` TEXT, `credential` TEXT, `cred_number` INTEGER NOT NULL, `revealed_attributes` TEXT NOT NULL, `finalized_balance` TEXT NOT NULL, `balance_at_disposal` TEXT NOT NULL, `total_shielded_balance` TEXT NOT NULL, `finalized_encrypted_balance` TEXT, `current_balance_status` INTEGER NOT NULL, `read_only` INTEGER NOT NULL, `finalized_account_release_schedule` TEXT, `cooldowns` TEXT NOT NULL, `account_delegation` TEXT, `account_baker` TEXT, `accountIndex` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identityId", + "columnName": "identity_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submission_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "transactionStatus", + "columnName": "transaction_status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedAccountData", + "columnName": "encrypted_account_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "credential", + "columnName": "credential", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "credNumber", + "columnName": "cred_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "revealedAttributes", + "columnName": "revealed_attributes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "balance", + "columnName": "finalized_balance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "balanceAtDisposal", + "columnName": "balance_at_disposal", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shieldedBalance", + "columnName": "total_shielded_balance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedBalance", + "columnName": "finalized_encrypted_balance", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedBalanceStatus", + "columnName": "current_balance_status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readOnly", + "columnName": "read_only", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseSchedule", + "columnName": "finalized_account_release_schedule", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cooldowns", + "columnName": "cooldowns", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "delegation", + "columnName": "account_delegation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "baker", + "columnName": "account_baker", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "index", + "columnName": "accountIndex", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_account_table_address", + "unique": true, + "columnNames": [ + "address" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_account_table_address` ON `${TABLE_NAME}` (`address`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "transfer_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `amount` TEXT NOT NULL, `cost` TEXT NOT NULL, `from_address` TEXT NOT NULL, `to_address` TEXT NOT NULL, `expiry` INTEGER NOT NULL, `memo` TEXT, `created_at` INTEGER NOT NULL, `submission_id` TEXT NOT NULL, `transaction_status` INTEGER NOT NULL, `outcome` INTEGER NOT NULL, `transactionType` TEXT NOT NULL, `newSelfEncryptedAmount` TEXT, `newStartIndex` INTEGER NOT NULL, `nonce` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cost", + "columnName": "cost", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromAddress", + "columnName": "from_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "toAddress", + "columnName": "to_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "expiry", + "columnName": "expiry", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "memo", + "columnName": "memo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submission_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "transactionStatus", + "columnName": "transaction_status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "outcome", + "columnName": "outcome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "transactionType", + "columnName": "transactionType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "newSelfEncryptedAmount", + "columnName": "newSelfEncryptedAmount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "newStartIndex", + "columnName": "newStartIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nonce", + "columnName": "nonce", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipient_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "encrypted_amount_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`encryptedkey` TEXT NOT NULL, `amount` TEXT, PRIMARY KEY(`encryptedkey`))", + "fields": [ + { + "fieldPath": "encryptedkey", + "columnName": "encryptedkey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "encryptedkey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "contract_token_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contract_index` TEXT NOT NULL, `contract_name` TEXT NOT NULL DEFAULT '', `token_id` TEXT NOT NULL, `account_address` TEXT, `is_fungible` INTEGER NOT NULL, `token_metadata` TEXT, `is_newly_received` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contractIndex", + "columnName": "contract_index", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contractName", + "columnName": "contract_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "token", + "columnName": "token_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountAddress", + "columnName": "account_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFungible", + "columnName": "is_fungible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tokenMetadata", + "columnName": "token_metadata", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isNewlyReceived", + "columnName": "is_newly_received", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_contract_token_table_contract_index_token_id_account_address", + "unique": true, + "columnNames": [ + "contract_index", + "token_id", + "account_address" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_contract_token_table_contract_index_token_id_account_address` ON `${TABLE_NAME}` (`contract_index`, `token_id`, `account_address`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8e6839ae303bff1c2c78e11537326637')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.concordium.wallet.data.room.app.AppDatabase/1.json b/app/schemas/com.concordium.wallet.data.room.app.AppDatabase/1.json new file mode 100644 index 00000000..0036a075 --- /dev/null +++ b/app/schemas/com.concordium.wallet.data.room.app.AppDatabase/1.json @@ -0,0 +1,71 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "57f1ba95820dc5b52f6358ac3399a98c", + "entities": [ + { + "tableName": "wallets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `is_active` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "is_active", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_wallets_created_at", + "unique": false, + "columnNames": [ + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_wallets_created_at` ON `${TABLE_NAME}` (`created_at`)" + }, + { + "name": "index_wallets_is_active", + "unique": false, + "columnNames": [ + "is_active" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_wallets_is_active` ON `${TABLE_NAME}` (`is_active`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '57f1ba95820dc5b52f6358ac3399a98c')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperTest.kt b/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperTest.kt new file mode 100644 index 00000000..41ea4e41 --- /dev/null +++ b/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperTest.kt @@ -0,0 +1,230 @@ +package com.concordium.wallet.core.security + +import com.concordium.wallet.data.model.EncryptedData +import com.concordium.wallet.util.toHex +import kotlinx.coroutines.runBlocking +import okio.ByteString.Companion.decodeHex +import org.junit.Assert +import org.junit.Test + +class EncryptionHelperTest { + private val sharedKey = "12345678912345678912345678912345".decodeHex().toByteArray() + + @Test + fun encryptSuccessfully() { + val data = "This should be kept in secret 🤫".toByteArray() + val (ed1, ed2) = runBlocking { + List(2) { + EncryptionHelper.encrypt( + key = sharedKey, + data = data, + ).also(::println) + } + } + + Assert.assertNotEquals( + "Cipher text must not be repeated", + ed1.ciphertext, ed2.ciphertext + ) + Assert.assertNotEquals( + "IV must not be reused", + ed1.iv, ed2.iv + ) + Assert.assertEquals( + "Transformation must remain constant", + ed1.transformation, ed2.transformation + ) + Assert.assertEquals("AES/GCM/NoPadding", ed1.transformation) + } + + @Test + fun encryptSuccessfully_IfNoData() { + val ed = runBlocking { + EncryptionHelper.encrypt( + key = sharedKey, + data = byteArrayOf(), + ).also(::println) + } + + Assert.assertEquals( + "Only the 128 bit tag must be present in the cipher text", + 128 / 8, ed.decodeCiphertext().size + ) + } + + @Test + fun decryptSuccessfully() { + val ed = EncryptedData( + ciphertext = "YQQvlr6O/Wr5YnLAS1wlNql6P/ovePjGe6ebH5tchLQ6Z9jz6aFAFONCktsFGihxlg4", + transformation = "AES/GCM/NoPadding", + iv = "QkQj9YofBqR7actQ", + ) + + val data = runBlocking { + EncryptionHelper.decrypt( + key = sharedKey, + encryptedData = ed, + ) + } + + Assert.assertEquals( + "This should be kept in secret 🤫", + String(data) + ) + } + + @Test + fun decryptSuccessfully_IfLegacyTransformation() { + val ed = EncryptedData( + ciphertext = "P1ZQet2bjxGmpGkzKOM680jgtnA+UOAo5ZYB3q7mwW7euZxMwwGF/yGtNm6zBYK3", + transformation = "AES/CBC/PKCS7Padding", + iv = "92b5AYQVTTr0fQXgmUAIAQ", + ) + + val data = runBlocking { + EncryptionHelper.decrypt( + key = sharedKey, + encryptedData = ed, + ) + } + + Assert.assertEquals( + "This should be kept in secret 🤫", + String(data) + ) + } + + @Test(expected = EncryptionException::class) + fun failToDecrypt_IfDifferentKey() { + val ed = EncryptedData( + ciphertext = "YQQvlr6O/Wr5YnLAS1wlNql6P/ovePjGe6ebH5tchLQ6Z9jz6aFAFONCktsFGihxlg4", + transformation = "AES/GCM/NoPadding", + iv = "QkQj9YofBqR7actQ", + ) + + runBlocking { + EncryptionHelper.decrypt( + key = byteArrayOf(1, 2, 3), + encryptedData = ed, + ) + } + } + + @Test(expected = EncryptionException::class) + fun failToDecrypt_IfDifferentIv() { + val ed = EncryptedData( + ciphertext = "YQQvlr6O/Wr5YnLAS1wlNql6P/ovePjGe6ebH5tchLQ6Z9jz6aFAFONCktsFGihxlg4", + transformation = "AES/GCM/NoPadding", + iv = "QkQj9YofCdR7actQ", + ) + + runBlocking { + EncryptionHelper.decrypt( + key = sharedKey, + encryptedData = ed, + ) + } + } + + @Test(expected = EncryptionException::class) + fun failToDecrypt_IfAlteredCipherText() { + val ed = EncryptedData( + ciphertext = "YQQvlr6O/Wr5YnLAS1wlNql6P/ovmPjGe6ebH5tchLQ6Z9jz6aFAFONCktsFGihxlg4", + transformation = "AES/GCM/NoPadding", + iv = "QkQj9YofBqR7actQ", + ) + + runBlocking { + EncryptionHelper.decrypt( + key = sharedKey, + encryptedData = ed, + ) + } + } + + @Test + fun generateKeySuccessfully() { + val (mc1, mc2) = runBlocking { + List(2) { + EncryptionHelper.generateKey().toHex().also(::println) + } + } + + Assert.assertNotEquals( + "Generated keys must not be repeated", + mc1, mc2 + ) + Assert.assertEquals( + "Generated key size must remain constant", + mc1.length, mc2.length + ) + Assert.assertTrue( + "Generated keys must be strong", + mc1.length >= 64, + ) + } + + @Test + fun generatePasswordKeySaltSuccessfully() { + val (s1, s2) = runBlocking { + List(2) { + EncryptionHelper.generatePasswordKeySalt().toHex().also(::println) + } + } + + Assert.assertNotEquals( + "Generated salt must not be repeated", + s1, s2 + ) + } + + @Test + fun generatePasswordKeySuccessfully() { + val p1 = "123456".toCharArray() + val p2 = "qwe123".toCharArray() + val s1 = "7804836792fbecf04961fe286e8ec950d2e105448e61a290262711154ec59026".decodeHex().toByteArray() + val s2 = "ac6e5196afd67b0e69c42fcba198420391b79a6ba9a916fa76a905f09017acc5".decodeHex().toByteArray() + + val (k1, k2) = runBlocking { + List(2) { + EncryptionHelper.generatePasswordKey( + password = p1, + salt = s1, + ).toHex().also(::println) + } + } + + Assert.assertEquals( + "Generated keys must remain constant if the inputs are constant", + k1, k2 + ) + Assert.assertTrue( + "Generated keys must be strong", + k1.length >= 64 + ) + + val k1s2 = runBlocking { + EncryptionHelper.generatePasswordKey( + password = p1, + salt = s2, + ).toHex() + } + + Assert.assertNotEquals( + "Keys generated with the same password but different salt must be different", + k1, k1s2 + ) + + val k2s1 = runBlocking { + EncryptionHelper.generatePasswordKey( + password = p2, + salt = s1, + ).toHex() + } + + Assert.assertNotEquals( + "Keys generated with the same salt but different passwords must be different", + k1, k2s1 + ) + } +} diff --git a/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperUnitTest.kt b/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperUnitTest.kt deleted file mode 100644 index 5b1b9daf..00000000 --- a/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperUnitTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.concordium.wallet.core.security - -import android.util.Base64 -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class EncryptionHelperUnitTest { - @Test - fun encryption() { - val password = "123" - val text = "some_text" - - val (salt, iv) = EncryptionHelper.createEncryptionData() - val decodedEncrypted = EncryptionHelper.encrypt(password, salt, iv, text) - - val toBeDecryptedByteArray = Base64.decode(decodedEncrypted, Base64.DEFAULT) - val decrypted = EncryptionHelper.decrypt(password, salt, iv, toBeDecryptedByteArray) - - // decrypting the encrypted gives the original - assertEquals(text, decrypted) - - // Same result every time - val decodedEncrypted2 = EncryptionHelper.encrypt(password, salt, iv, text) - assertEquals(decodedEncrypted, decodedEncrypted2) - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f292c8a5..701f7b65 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -207,35 +207,18 @@ android:theme="@style/CCX_Screen" android:windowSoftInputMode="stateAlwaysVisible|adjustResize" /> - - - @@ -562,6 +545,11 @@ android:launchMode="singleTop" android:screenOrientation="portrait" android:theme="@style/CCX_Screen"/> + diff --git a/app/src/main/java/com/concordium/wallet/App.kt b/app/src/main/java/com/concordium/wallet/App.kt index 39ba6922..292b1232 100644 --- a/app/src/main/java/com/concordium/wallet/App.kt +++ b/app/src/main/java/com/concordium/wallet/App.kt @@ -2,6 +2,7 @@ package com.concordium.wallet import android.app.Application import android.content.Context +import com.concordium.wallet.core.AppCore import com.concordium.wallet.core.notifications.AnnouncementNotificationManager import com.concordium.wallet.data.backend.ws.WsCreds import com.concordium.wallet.util.Log @@ -39,7 +40,7 @@ class App : Application() { } fun initAppCore() { - appCore = AppCore(this.applicationContext) + appCore = AppCore(this@App) } private fun initWalletConnect() { diff --git a/app/src/main/java/com/concordium/wallet/AppCore.kt b/app/src/main/java/com/concordium/wallet/AppCore.kt deleted file mode 100644 index 0def63f7..00000000 --- a/app/src/main/java/com/concordium/wallet/AppCore.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.concordium.wallet - -import android.content.Context -import com.concordium.wallet.core.authentication.AuthenticationManager -import com.concordium.wallet.core.authentication.Session -import com.concordium.wallet.core.crypto.CryptoLibrary -import com.concordium.wallet.core.crypto.CryptoLibraryReal -import com.concordium.wallet.core.gson.BigIntegerTypeAdapter -import com.concordium.wallet.core.gson.RawJsonTypeAdapter -import com.concordium.wallet.core.tracking.AppTracker -import com.concordium.wallet.core.tracking.MatomoAppTracker -import com.concordium.wallet.core.tracking.NoOpAppTracker -import com.concordium.wallet.data.backend.ProxyBackend -import com.concordium.wallet.data.backend.ProxyBackendConfig -import com.concordium.wallet.data.backend.airdrop.AirDropBackend -import com.concordium.wallet.data.backend.airdrop.AirDropBackendConfig -import com.concordium.wallet.data.backend.news.NewsfeedRssBackend -import com.concordium.wallet.data.backend.news.NewsfeedRssBackendConfig -import com.concordium.wallet.data.backend.notifications.NotificationsBackend -import com.concordium.wallet.data.backend.notifications.NotificationsBackendConfig -import com.concordium.wallet.data.backend.tokens.TokensBackend -import com.concordium.wallet.data.backend.tokens.TokensBackendConfig -import com.concordium.wallet.data.model.RawJson -import com.concordium.wallet.data.preferences.TrackingPreferences -import com.concordium.wallet.data.room.Identity -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import org.matomo.sdk.Matomo -import org.matomo.sdk.TrackerBuilder -import java.math.BigInteger - -class AppCore(val context: Context) { - - val gson: Gson = initializeGson() - val proxyBackendConfig = ProxyBackendConfig(gson) - private val tokenBackendConfig = TokensBackendConfig(gson) - private val airdropBackendConfig = AirDropBackendConfig(gson) - private val newsfeedRssBackendConfig: NewsfeedRssBackendConfig by lazy(::NewsfeedRssBackendConfig) - private val notificationsBackendConfig: NotificationsBackendConfig = - NotificationsBackendConfig(gson) - val cryptoLibrary: CryptoLibrary = CryptoLibraryReal(gson) - - private val trackingPreferences = TrackingPreferences(context) - private val noOpAppTracker: AppTracker = NoOpAppTracker() - private val matomoAppTracker: AppTracker by lazy { - TrackerBuilder.createDefault("https://concordium.matomo.cloud/matomo.php", 8) - .build(Matomo.getInstance(context)) - .let(::MatomoAppTracker) - } - val tracker: AppTracker - get() = - if (trackingPreferences.isTrackingEnabled) - matomoAppTracker - else - noOpAppTracker - - val session: Session = Session(App.appContext) - var newIdentities = mutableMapOf() - - private val authenticationManagerGeneric: AuthenticationManager = - AuthenticationManager(session.getBiometricAuthKeyName()) - private var authenticationManagerReset: AuthenticationManager = authenticationManagerGeneric - private var authenticationManager: AuthenticationManager = authenticationManagerGeneric - private var resetBiometricKeyNameAppendix: String = "" - - fun getNotificationsBackend(): NotificationsBackend { - return notificationsBackendConfig.backend - } - - fun getNewsfeedRssBackend(): NewsfeedRssBackend { - return newsfeedRssBackendConfig.backend - } - - fun getTokensBackend(): TokensBackend { - return tokenBackendConfig.backend - } - - fun getAirdropBackend(): AirDropBackend { - return airdropBackendConfig.backend - } - - fun getProxyBackend(): ProxyBackend { - return proxyBackendConfig.backend - } - - private fun initializeGson(): Gson { - val gsonBuilder = GsonBuilder() - gsonBuilder.registerTypeAdapter(RawJson::class.java, RawJsonTypeAdapter()) - gsonBuilder.registerTypeAdapter(BigInteger::class.java, BigIntegerTypeAdapter()) - return gsonBuilder.create() - } - - fun getOriginalAuthenticationManager(): AuthenticationManager { - return authenticationManagerReset - } - - fun getCurrentAuthenticationManager(): AuthenticationManager { - return authenticationManager - } - - fun startResetAuthFlow() { - resetBiometricKeyNameAppendix = System.currentTimeMillis().toString() - authenticationManagerReset = AuthenticationManager(resetBiometricKeyNameAppendix) - authenticationManager = authenticationManagerReset - } - - fun finalizeResetAuthFlow() { - session.setBiometricAuthKeyName(resetBiometricKeyNameAppendix) - session.hasFinishedSetupPassword() - } - - fun cancelResetAuthFlow() { - authenticationManagerReset = authenticationManagerGeneric - authenticationManager = authenticationManagerReset - session.hasFinishedSetupPassword() - } -} diff --git a/app/src/main/java/com/concordium/wallet/core/AppAuth.kt b/app/src/main/java/com/concordium/wallet/core/AppAuth.kt new file mode 100644 index 00000000..9834cb7a --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/AppAuth.kt @@ -0,0 +1,289 @@ +package com.concordium.wallet.core + +import com.concordium.wallet.core.security.EncryptionException +import com.concordium.wallet.core.security.EncryptionHelper +import com.concordium.wallet.core.security.KeystoreEncryptionException +import com.concordium.wallet.core.security.KeystoreHelper +import com.concordium.wallet.data.model.EncryptedData +import com.concordium.wallet.data.preferences.AppSetupPreferences +import com.concordium.wallet.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.IllegalBlockSizeException + +/** + * Handles app-wide password auth and encryption. + * + * In the app, all the encryption is done with a single master key. + * The master key is stored encrypted with a key derived from the password. + * If the biometric auth is set up, the password is also stored encrypted + * with a key from Android keystore and a dedicated cipher for biometrics. + * + * When the password is changed, only the master key gets re-encrypted + * as well as the encrypted password for biometrics, if set up. + * But all the encrypted data remains intact. + * + * @param slot identifier of a slot to read/write the auth data into. + * Use AuthenticationManagers with different slots to safely update the auth + * in the same way the A/B flashing works in smartphones. + * + * @see commitCurrentSlot to save changes done in a given slot + */ +class AppAuth( + private val appSetupPreferences: AppSetupPreferences, + private val slot: String = appSetupPreferences.getCurrentAuthSlot(), +) { + // region Biometrics + + fun initBiometricAuth( + password: String, + cipher: Cipher, + ): Boolean { + try { + appSetupPreferences.setEncryptedPassword( + slot, + EncryptedData( + ciphertext = cipher.doFinal(password.toByteArray()), + iv = cipher.iv, + transformation = cipher.algorithm, + ) + ) + appSetupPreferences.setUseBiometrics(slot, true) + return true + } catch (e: java.lang.Exception) { + when (e) { + is BadPaddingException, + is IllegalBlockSizeException, + -> { + Log.e("Failed to encrypt the data with the generated key. ${e.message}") + return false + } + + else -> throw e + } + } + } + + fun generateBiometricsSecretKey(): Boolean { + return try { + KeystoreHelper().generateSecretKey(slot) + true + } catch (e: KeystoreEncryptionException) { + false + } + } + + /** + * Can throw KeystoreEncryptionException or return null in case of KeyPermanentlyInvalidatedException + */ + fun getBiometricsCipherForEncryption(): Cipher? { + return KeystoreHelper().initCipherForEncryption(slot) + } + + /** + * Can throw KeystoreEncryptionException or return null in case of KeyPermanentlyInvalidatedException + */ + fun getBiometricsCipherForDecryption(): Cipher? { + try { + val cipher = KeystoreHelper().initCipherForDecryption( + keyName = slot, + initVector = appSetupPreferences.getEncryptedPassword(slot).decodeIv(), + ) + + if (cipher == null) { + appSetupPreferences.setUseBiometrics(slot, false) + } + + return cipher + } catch (e: KeystoreEncryptionException) { + appSetupPreferences.setUseBiometrics(slot, false) + throw e + } + } + + suspend fun decryptPasswordWithBiometricsCipher( + cipher: Cipher, + ): String? = withContext(Dispatchers.Default) { + runCatching { + val encryptedPassword = appSetupPreferences.getEncryptedPassword(slot) + cipher.doFinal(encryptedPassword.decodeCiphertext()) + } + .getOrNull() + ?.let(::String) + } + + //endregion + + /** + * Initializes the password auth generating a new encryption master key. + * Use this method to init the auth for the first time. + */ + @Throws(EncryptionException::class) + suspend fun initPasswordAuth( + password: String, + isPasscode: Boolean, + ) = + initPasswordAuth( + password = password, + isPasscode = isPasscode, + masterKey = EncryptionHelper.generateKey(), + ) + + /** + * Initializes the password auth reusing the existing master key. + * Use this method in combination with the current master key + * to change the existing password. + */ + @Throws(EncryptionException::class) + suspend fun initPasswordAuth( + password: String, + isPasscode: Boolean, + masterKey: ByteArray, + ) { + val passwordKeySalt = EncryptionHelper.generatePasswordKeySalt() + val passwordKey = EncryptionHelper.generatePasswordKey( + password = password.toCharArray(), + salt = passwordKeySalt, + ) + val encryptedMasterKey: EncryptedData = EncryptionHelper.encrypt( + key = passwordKey, + data = masterKey, + ) + appSetupPreferences.setPasswordKeySalt(slot, passwordKeySalt) + appSetupPreferences.setEncryptedMasterKey(slot, encryptedMasterKey) + appSetupPreferences.setUsePasscode(slot, isPasscode) + } + + /** + * Saves the current slot as the main one, + * therefore committing all the changes done in this slot. + */ + fun commitCurrentSlot() { + appSetupPreferences.setCurrentAuthSlot(slot) + } + + @Throws(EncryptionException::class) + suspend fun getMasterKey( + password: String, + ): ByteArray { + val passwordKey = EncryptionHelper.generatePasswordKey( + password = password.toCharArray(), + salt = appSetupPreferences.getPasswordKeySalt(slot), + ) + return getMasterKey(passwordKey) + } + + @Throws(EncryptionException::class) + private suspend fun getMasterKey( + passwordKey: ByteArray, + ): ByteArray { + val encryptedMasterKey = appSetupPreferences.getEncryptedMasterKey(slot) + return EncryptionHelper.decrypt( + key = passwordKey, + encryptedData = encryptedMasterKey + ) + } + + suspend fun checkPassword(password: String): Boolean { + val passwordKey = EncryptionHelper.generatePasswordKey( + password = password.toCharArray(), + salt = appSetupPreferences.getPasswordKeySalt(slot), + ) + + if (runCatching { getMasterKey(passwordKey) }.getOrNull() != null) { + return true + } + + return checkPasswordLegacySavingMasterKey(passwordKey) + } + + private suspend fun checkPasswordLegacySavingMasterKey(passwordKey: ByteArray): Boolean { + // Legacy password check is a string and its encrypted representation, + // which must be compared once decrypted. + // It was in place before the Two wallets feature. + + val legacyPasswordCheck = appSetupPreferences.getLegacyPasswordCheck(slot) + ?: return false + val legacyEncryptedPasswordCheck = appSetupPreferences.getLegacyEncryptedPasswordCheck(slot) + ?: return false + val legacyDecryptedPasswordCheck = runCatching { + EncryptionHelper.decrypt( + key = passwordKey, + encryptedData = legacyEncryptedPasswordCheck, + ) + }.getOrNull() ?: return false + + if (String(legacyDecryptedPasswordCheck) == legacyPasswordCheck) { + // If the password is correct, then the current master key is the password key. + // This was the approach to the encryption before the Two wallets feature. + val encryptedMasterKey: EncryptedData = EncryptionHelper.encrypt( + key = passwordKey, + data = passwordKey, + ) + appSetupPreferences.setEncryptedMasterKey(slot, encryptedMasterKey) + } else { + return false + } + return String(legacyDecryptedPasswordCheck) == legacyPasswordCheck + } + + suspend fun encrypt( + password: String, + data: ByteArray, + ): EncryptedData? = + runCatching { + encrypt( + masterKey = getMasterKey(password), + data = data, + ) + }.getOrNull() + + suspend fun encrypt( + masterKey: ByteArray, + data: ByteArray, + ): EncryptedData? = + runCatching { + EncryptionHelper.encrypt( + key = masterKey, + data = data, + ) + }.getOrNull() + + suspend fun decrypt( + password: String, + encryptedData: EncryptedData, + ): ByteArray? = + runCatching { + decrypt( + masterKey = getMasterKey(password), + encryptedData = encryptedData, + ) + }.getOrNull() + + suspend fun decrypt( + masterKey: ByteArray, + encryptedData: EncryptedData, + ): ByteArray? = + runCatching { + EncryptionHelper.decrypt( + key = masterKey, + encryptedData = encryptedData, + ) + }.getOrNull() + + fun isPasswordAuthInitialized(): Boolean { + return (appSetupPreferences.hasEncryptedMasterKey(slot) + || appSetupPreferences.getLegacyPasswordCheck(slot) != null) + && appSetupPreferences.hasPasswordKeySalt(slot) + } + + fun isPasscodeUsed(): Boolean { + return appSetupPreferences.getUsePasscode(slot) + } + + fun isBiometricsUsed(): Boolean { + return appSetupPreferences.getUseBiometrics(slot) + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/AppCore.kt b/app/src/main/java/com/concordium/wallet/core/AppCore.kt new file mode 100644 index 00000000..f20fc880 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/AppCore.kt @@ -0,0 +1,148 @@ +package com.concordium.wallet.core + +import android.os.Handler +import com.concordium.wallet.App +import com.concordium.wallet.core.crypto.CryptoLibrary +import com.concordium.wallet.core.crypto.CryptoLibraryReal +import com.concordium.wallet.core.gson.BigIntegerTypeAdapter +import com.concordium.wallet.core.gson.RawJsonTypeAdapter +import com.concordium.wallet.core.migration.TwoWalletsMigration +import com.concordium.wallet.core.multiwallet.AppWallet +import com.concordium.wallet.core.tracking.AppTracker +import com.concordium.wallet.core.tracking.MatomoAppTracker +import com.concordium.wallet.core.tracking.NoOpAppTracker +import com.concordium.wallet.data.AppWalletRepository +import com.concordium.wallet.data.backend.ProxyBackend +import com.concordium.wallet.data.backend.ProxyBackendConfig +import com.concordium.wallet.data.backend.airdrop.AirDropBackend +import com.concordium.wallet.data.backend.airdrop.AirDropBackendConfig +import com.concordium.wallet.data.backend.news.NewsfeedRssBackend +import com.concordium.wallet.data.backend.news.NewsfeedRssBackendConfig +import com.concordium.wallet.data.backend.notifications.NotificationsBackend +import com.concordium.wallet.data.backend.notifications.NotificationsBackendConfig +import com.concordium.wallet.data.backend.tokens.TokensBackend +import com.concordium.wallet.data.backend.tokens.TokensBackendConfig +import com.concordium.wallet.data.model.RawJson +import com.concordium.wallet.data.preferences.AppSetupPreferences +import com.concordium.wallet.data.preferences.AppTrackingPreferences +import com.concordium.wallet.data.room.app.AppDatabase +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.runBlocking +import org.matomo.sdk.Matomo +import org.matomo.sdk.TrackerBuilder +import java.math.BigInteger +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class AppCore(val app: App) { + + val gson: Gson = getGson() + val proxyBackendConfig = ProxyBackendConfig(gson) + private val tokenBackendConfig = TokensBackendConfig(gson) + private val airdropBackendConfig = AirDropBackendConfig(gson) + private val newsfeedRssBackendConfig: NewsfeedRssBackendConfig by lazy(::NewsfeedRssBackendConfig) + private val notificationsBackendConfig: NotificationsBackendConfig = + NotificationsBackendConfig(gson) + val cryptoLibrary: CryptoLibrary = CryptoLibraryReal(gson) + private val appTrackingPreferences = AppTrackingPreferences(App.appContext) + private val noOpAppTracker: AppTracker = NoOpAppTracker() + private val matomoAppTracker: AppTracker by lazy { + TrackerBuilder.createDefault("https://concordium.matomo.cloud/matomo.php", 8) + .build(Matomo.getInstance(App.appContext)) + .let(::MatomoAppTracker) + } + val tracker: AppTracker + get() = + if (appTrackingPreferences.isTrackingEnabled) + matomoAppTracker + else + noOpAppTracker + + // Migrations are invoked once vital core components are initialized: + // Gson, crypto lib, etc. + init { + with(TwoWalletsMigration(app, gson)) { + if (isPreferenceMigrationNeeded()) { + migratePreferencesOnce() + } + + runBlocking { + if (isAppDatabaseMigrationNeeded()) { + migrateAppDatabaseOnce() + } + } + } + } + + val database = AppDatabase.getDatabase(app) + val walletRepository = AppWalletRepository(database.appWalletDao()) + + var session: Session = + runBlocking { + Session( + context = app, + activeWallet = walletRepository.getActiveWallet(), + ) + } + private set + + val setup = AppSetup( + appSetupPreferences = AppSetupPreferences( + context = App.appContext, + gson = gson, + ), + getSession = { session }, + ) + val auth: AppAuth + get() = setup.auth + + fun getNotificationsBackend(): NotificationsBackend { + return notificationsBackendConfig.backend + } + + fun getNewsfeedRssBackend(): NewsfeedRssBackend { + return newsfeedRssBackendConfig.backend + } + + fun getTokensBackend(): TokensBackend { + return tokenBackendConfig.backend + } + + fun getAirdropBackend(): AirDropBackend { + return airdropBackendConfig.backend + } + + fun getProxyBackend(): ProxyBackend { + return proxyBackendConfig.backend + } + + suspend fun startNewSession( + activeWallet: AppWallet, + isLoggedIn: Boolean = session.isLoggedIn.value == true, + ) = suspendCoroutine { continuation -> + // Session must be created in the main thread as it contains logout timer. + Handler(app.mainLooper).post { + continuation.context.ensureActive() + + session.inactivityCountDownTimer.cancel() + session = Session( + context = app, + activeWallet = activeWallet, + isLoggedIn = isLoggedIn, + ) + + continuation.resume(Unit) + } + } + + companion object { + fun getGson(): Gson { + val gsonBuilder = GsonBuilder() + gsonBuilder.registerTypeAdapter(RawJson::class.java, RawJsonTypeAdapter()) + gsonBuilder.registerTypeAdapter(BigInteger::class.java, BigIntegerTypeAdapter()) + return gsonBuilder.create() + } + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/AppSetup.kt b/app/src/main/java/com/concordium/wallet/core/AppSetup.kt new file mode 100644 index 00000000..2283c246 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/AppSetup.kt @@ -0,0 +1,66 @@ +package com.concordium.wallet.core + +import com.concordium.wallet.data.preferences.AppSetupPreferences + +class AppSetup( + private val appSetupPreferences: AppSetupPreferences, + private val getSession: () -> Session, +) { + var auth = AppAuth(appSetupPreferences) + private set + private var oldAuth = auth + var authSetupPassword: String? = null + private set + var authResetMasterKey: ByteArray? = null + private set + val isInitialSetupCompleted: Boolean + get() = appSetupPreferences.getHasCompletedInitialSetup() + val isAuthSetupCompleted: Boolean + get() = auth.isPasswordAuthInitialized() + + fun finishInitialSetup() { + appSetupPreferences.setHasCompletedInitialSetup(true) + } + + fun beginAuthSetup(password: String) { + authSetupPassword = password + } + + fun finishAuthSetup() { + authSetupPassword = null + getSession().setUserLoggedIn() + } + + /** + * Allows manipulating the [auth] without making the change permanent + * until [commitAuthReset] is called. In case of failure, call [cancelAuthReset]. + * + * @param decryptedMasterKey to be used for during the reset, see [authResetMasterKey] + */ + fun beginAuthReset(decryptedMasterKey: ByteArray) { + // Back up the current auth manager and replace it + // with the one with an alternative slot. + oldAuth = auth + auth = AppAuth( + appSetupPreferences = appSetupPreferences, + slot = System.currentTimeMillis().toString() + ) + // Store the decrypted master key in memory + // to re-init the auth with it later. + this.authResetMasterKey = decryptedMasterKey + } + + fun commitAuthReset() { + authResetMasterKey = null + // Save the current (alternative) slot and continue using it. + auth.commitCurrentSlot() + finishAuthSetup() + } + + fun cancelAuthReset() { + authResetMasterKey = null + // Restore the auth manager discarding the alternative slot. + auth = oldAuth + finishAuthSetup() + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/Session.kt b/app/src/main/java/com/concordium/wallet/core/Session.kt new file mode 100644 index 00000000..3b3fbcc4 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/Session.kt @@ -0,0 +1,93 @@ +package com.concordium.wallet.core + +import android.content.Context +import android.os.CountDownTimer +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.concordium.wallet.core.multiwallet.AppWallet +import com.concordium.wallet.data.WalletStorage +import com.concordium.wallet.data.preferences.Preferences +import com.concordium.wallet.data.room.Identity + +class Session( + context: Context, + val activeWallet: AppWallet, + isLoggedIn: Boolean = false, +) { + val walletStorage = WalletStorage( + activeWallet = activeWallet, + context = context, + ) + var newIdentities = mutableMapOf() + + private val _isLoggedIn = MutableLiveData(isLoggedIn) + val isLoggedIn: LiveData + get() = _isLoggedIn + + // The notice must be shown once per app start. + private var isUnshieldingNoticeShown = false + + fun setHasShowRewards(id: Int, value: Boolean) { + walletStorage.filterPreferences.setHasShowRewards(id, value) + } + + fun getHasShowRewards(id: Int): Boolean { + return walletStorage.filterPreferences.getHasShowRewards(id) + } + + fun setHasShowFinalizationRewards(id: Int, value: Boolean) { + walletStorage.filterPreferences.setHasShowFinalizationRewards(id, value) + } + + fun getHasShowFinalizationRewards(id: Int): Boolean { + return walletStorage.filterPreferences.getHasShowFinalizationRewards(id) + } + + fun setUnshieldingNoticeShown() { + isUnshieldingNoticeShown = true + } + + fun isUnshieldingNoticeShown(): Boolean = + isUnshieldingNoticeShown + + fun setUserLoggedIn() { + _isLoggedIn.value = true + resetLogoutTimeout() + } + + fun resetLogoutTimeout() { + if (_isLoggedIn.value!!) { + inactivityCountDownTimer.cancel() + inactivityCountDownTimer.start() + } + } + + var inactivityCountDownTimer = + object : CountDownTimer(60 * 5 * 1000.toLong(), 1000) { + override fun onTick(millisUntilFinished: Long) {} + + override fun onFinish() { + _isLoggedIn.value = false + } + } + + fun isAccountsBackupPossible(): Boolean { + return activeWallet.type == AppWallet.Type.FILE + } + + fun areAccountsBackedUp(): Boolean { + return walletStorage.setupPreferences.areAccountsBackedUp() + } + + fun setAccountsBackedUp(value: Boolean) { + return walletStorage.setupPreferences.setAccountsBackedUp(value) + } + + fun addAccountsBackedUpListener(listener: Preferences.Listener) { + walletStorage.setupPreferences.addAccountsBackedUpListener(listener) + } + + fun removeAccountsBackedUpListener(listener: Preferences.Listener) { + walletStorage.setupPreferences.removeListener(listener) + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/authentication/AuthenticationManager.kt b/app/src/main/java/com/concordium/wallet/core/authentication/AuthenticationManager.kt deleted file mode 100644 index 68703f5c..00000000 --- a/app/src/main/java/com/concordium/wallet/core/authentication/AuthenticationManager.kt +++ /dev/null @@ -1,263 +0,0 @@ -package com.concordium.wallet.core.authentication - -import android.util.Base64 -import com.concordium.wallet.App -import com.concordium.wallet.core.security.EncryptionException -import com.concordium.wallet.core.security.EncryptionHelper -import com.concordium.wallet.core.security.KeystoreEncryptionException -import com.concordium.wallet.core.security.KeystoreHelper -import com.concordium.wallet.data.preferences.AuthPreferences -import com.concordium.wallet.util.Log -import com.concordium.wallet.util.RandomUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import javax.crypto.BadPaddingException -import javax.crypto.Cipher -import javax.crypto.IllegalBlockSizeException -import javax.crypto.SecretKey - -class AuthenticationManager(biometricKeyName: String) { - - private val t = this.javaClass.simpleName - - private val biometricKeyName: String = biometricKeyName - private val authPreferences = AuthPreferences(App.appContext) - - //region Biometrics - - fun setupBiometrics(password: String, cipher: Cipher): Boolean { - try { - val initVector = cipher.iv - val encrypted = cipher.doFinal(password.toByteArray()) - val encodedEncryptedPassword = Base64.encodeToString(encrypted, Base64.DEFAULT) - authPreferences.setEncryptedPassword(biometricKeyName, encodedEncryptedPassword) - authPreferences.setEncryptedPasswordDerivedKeyInitVector( - biometricKeyName, - Base64.encodeToString( - initVector, - Base64.DEFAULT - ) - ) - authPreferences.setUseBiometrics(biometricKeyName, true) - return true - } catch (e: java.lang.Exception) { - when (e) { - is BadPaddingException, - is IllegalBlockSizeException -> { - Log.e("Failed to encrypt the data with the generated key. ${e.message}") - return false - } - else -> throw e - } - } - } - - fun generateBiometricsSecretKey(): Boolean { - return try { - KeystoreHelper().generateSecretKey(biometricKeyName) - true - } catch (e: KeystoreEncryptionException) { - false - } - } - - /** - * Can throw KeystoreEncryptionException or return null in case of KeyPermanentlyInvalidatedException - */ - fun initBiometricsCipherForEncryption(): Cipher? { - return KeystoreHelper().initCipherForEncryption(biometricKeyName) - } - - /** - * Can throw KeystoreEncryptionException or return null in case of KeyPermanentlyInvalidatedException - */ - fun initBiometricsCipherForDecryption(): Cipher? { - try { - val initVector = authPreferences.getBiometricsKeyEncryptionInitVector(biometricKeyName) - val cipher = KeystoreHelper().initCipherForDecryption( - biometricKeyName, - Base64.decode(initVector, Base64.DEFAULT) - ) - - if (cipher == null) { - authPreferences.setUseBiometrics(biometricKeyName, false) - } - return cipher - } catch (e: KeystoreEncryptionException) { - authPreferences.setUseBiometrics(biometricKeyName, false) - throw e - } - } - - //endregion - - fun createPasswordCheck(password: String): Boolean { - // Create encryption data used for encryption and decryption with password derived key - try { - val (salt, iv) = EncryptionHelper.createEncryptionData() - // Save encryption data - val encodedSalt = Base64.encodeToString(salt, Base64.DEFAULT) - val encodedIV = Base64.encodeToString(iv, Base64.DEFAULT) - authPreferences.setPasswordEncryptionSalt(biometricKeyName, encodedSalt) - authPreferences.setPasswordEncryptionInitVector(biometricKeyName, encodedIV) - - val passwordCheck = RandomUtil.randomString(20) - Log.d("PasswordCheck: $passwordCheck") - - // Derive password key and encrypt password check - val result = EncryptionHelper.encrypt(password, salt, iv, passwordCheck) - authPreferences.setPasswordCheckEncrypted(biometricKeyName, result) - authPreferences.setPasswordCheck(biometricKeyName, passwordCheck) - return true - } catch (e: EncryptionException) { - return false - } - } - - fun checkPassword(password: String): Boolean { - Log.i("$t#checkPassword -> password: $password") - val encodedSalt = authPreferences.getPasswordEncryptionSalt(biometricKeyName) - Log.i("$t#checkPassword -> encodedSalt: $encodedSalt") - val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName) - Log.i("$t#checkPassword -> encodedIV: $encodedIV") - val encodedPasswordCheckEncrypted = authPreferences.getPasswordCheckEncrypted(biometricKeyName) - Log.i("$t#checkPassword -> encodedPasswordCheckEncrypted: $encodedPasswordCheckEncrypted") - val salt = Base64.decode(encodedSalt, Base64.DEFAULT) - Log.i("$t#checkPassword -> salt: $salt") - val iv = Base64.decode(encodedIV, Base64.DEFAULT) - Log.i("$t#checkPassword -> iv: $iv") - val passwordCheckEncrypted = Base64.decode(encodedPasswordCheckEncrypted, Base64.DEFAULT) - Log.i("$t#checkPassword -> passwordCheckEncrypted: $passwordCheckEncrypted") - // Derive password key and decrypt password check - try { - val decryptedString = - EncryptionHelper.decrypt(password, salt, iv, passwordCheckEncrypted) - val savedPasswordCheck = authPreferences.getPasswordCheck(biometricKeyName) - return decryptedString.equals(savedPasswordCheck) - } catch (e: EncryptionException) { - return false - } - } - - suspend fun checkPasswordInBackground(password: String): Boolean = withContext(Dispatchers.Default) { - return@withContext checkPassword(password) - } - - suspend fun checkPasswordInBackground(cipher: Cipher): String? = - withContext(Dispatchers.Default) { - try { - val encodedEncryptedPassword = getEncryptedPassword() - var encryptedPassword = Base64.decode(encodedEncryptedPassword, Base64.DEFAULT) - - val decryptedByteArray = cipher.doFinal(encryptedPassword) - val decryptedPasswordString = String(decryptedByteArray, charset("UTF-8")) - - val res = checkPassword(decryptedPasswordString) - return@withContext if (res) { - decryptedPasswordString - } else { - null - } - } catch (e: Exception) { - when (e) { - is BadPaddingException, - is IllegalBlockSizeException -> { - Log.e("Failed to decrypt the data with the generated key. ${e.message}") - return@withContext null - } - else -> throw e - } - } - } - - suspend fun encryptInBackground(password: String, toBeEncrypted: String): String? = - withContext(Dispatchers.Default) { - val encodedSalt = authPreferences.getPasswordEncryptionSalt(biometricKeyName) - val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName) - val salt = Base64.decode(encodedSalt, Base64.DEFAULT) - val iv = Base64.decode(encodedIV, Base64.DEFAULT) - - // Derive password key and encrypt - try { - val encodedEncrypted = EncryptionHelper.encrypt(password, salt, iv, toBeEncrypted) - return@withContext encodedEncrypted - } catch (e: EncryptionException) { - return@withContext null - } - } - - suspend fun decryptInBackground(password: String, encodedToBeDecrypted: String): String? = - withContext(Dispatchers.Default) { - val encodedSalt = authPreferences.getPasswordEncryptionSalt(biometricKeyName) - val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName) - val salt = Base64.decode(encodedSalt, Base64.DEFAULT) - val iv = Base64.decode(encodedIV, Base64.DEFAULT) - - // Derive password key and decrypt - val toBeDecryptedByteArray = Base64.decode(encodedToBeDecrypted, Base64.DEFAULT) - try { - val decrypted = EncryptionHelper.decrypt(password, salt, iv, toBeDecryptedByteArray) - return@withContext decrypted - } catch (e: EncryptionException) { - return@withContext null - } - } - - suspend fun derivePasswordKeyInBackground(password: String): SecretKey? = withContext(Dispatchers.Default) { - Log.i("$t#derivePasswordKeyInBackground: password -> $password") - val encodedSalt = authPreferences.getPasswordEncryptionSalt(biometricKeyName) - Log.i("$t#derivePasswordKeyInBackground: encodedSalt -> $encodedSalt") - val salt = Base64.decode(encodedSalt, Base64.DEFAULT) - Log.i("$t#derivePasswordKeyInBackground: salt -> $salt") - // Derive password key - try { - val key = EncryptionHelper.generateKey(password, salt) - return@withContext key - } catch (e: EncryptionException) { - return@withContext null - } - } - - suspend fun encryptInBackground(key: SecretKey, toBeEncrypted: String): String? = withContext(Dispatchers.Default) { - val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName) - val iv = Base64.decode(encodedIV, Base64.DEFAULT) - - // Derive password key and encrypt - try { - val encodedEncrypted = EncryptionHelper.encrypt(key, iv, toBeEncrypted) - return@withContext encodedEncrypted - } catch (e: EncryptionException) { - return@withContext null - } - } - - suspend fun decryptInBackground(key: SecretKey, encodedToBeDecrypted: String): String? = withContext(Dispatchers.Default) { - val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName) - val iv = Base64.decode(encodedIV, Base64.DEFAULT) - - // Derive password key and decrypt - val toBeDecryptedByteArray = Base64.decode(encodedToBeDecrypted, Base64.DEFAULT) - try { - val decrypted = EncryptionHelper.decrypt(key, iv, toBeDecryptedByteArray) - return@withContext decrypted - } catch (e: EncryptionException) { - return@withContext null - } - } - - fun usePasscode(): Boolean { - return authPreferences.getUsePasscode(biometricKeyName) - } - - fun useBiometrics(): Boolean { - return authPreferences.getUseBiometrics(biometricKeyName) - } - - fun getEncryptedPassword(): String { - return authPreferences.getEncryptedPassword(biometricKeyName) - } - - fun setUsePassCode(passcodeUsed: Boolean) { - authPreferences.setUsePasscode(biometricKeyName, passcodeUsed) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/concordium/wallet/core/authentication/Session.kt b/app/src/main/java/com/concordium/wallet/core/authentication/Session.kt deleted file mode 100644 index 06cc35fc..00000000 --- a/app/src/main/java/com/concordium/wallet/core/authentication/Session.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.concordium.wallet.core.authentication - -import android.content.Context -import android.os.CountDownTimer -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.concordium.wallet.App -import com.concordium.wallet.data.preferences.AuthPreferences -import com.concordium.wallet.data.preferences.FilterPreferences -import com.concordium.wallet.data.preferences.Preferences - -class Session { - - private var authPreferences: AuthPreferences - private var filterPreferences: FilterPreferences - - var hasSetupPassword = false - private set - - var tempPassword: String? = null - private set - - /** - * The value is positive at the fresh app start until the setup start screen is visited. - */ - var hasCompletedInitialSetup = true - private set - - var hasCompleteOnboarding = false - private set - - private val _isLoggedIn = MutableLiveData(false) - val isLoggedIn: LiveData - get() = _isLoggedIn - - // The notice must be shown once per app start. - private var isUnshieldingNoticeShown = false - - constructor(context: Context) { - authPreferences = AuthPreferences(context) - hasSetupPassword = authPreferences.getHasSetupUser() - hasCompletedInitialSetup = authPreferences.getHasCompletedInitialSetup() - hasCompleteOnboarding = authPreferences.getHasCompletedOnboarding() - filterPreferences = FilterPreferences(context) - } - - fun setHasShowRewards(id: Int, value: Boolean) { - filterPreferences.setHasShowRewards(id, value) - } - - fun getHasShowRewards(id: Int): Boolean { - return filterPreferences.getHasShowRewards(id) - } - - fun setHasShowFinalizationRewards(id: Int, value: Boolean) { - filterPreferences.setHasShowFinalizationRewards(id, value) - } - - fun getHasShowFinalizationRewards(id: Int): Boolean { - return filterPreferences.getHasShowFinalizationRewards(id) - } - - fun unshieldingNoticeShown() { - isUnshieldingNoticeShown = true - } - - fun isUnshieldingNoticeShown(): Boolean = - isUnshieldingNoticeShown - - fun hasSetupPassword(passcodeUsed: Boolean = false) { - _isLoggedIn.value = true - authPreferences.setHasSetupUser(true) - App.appCore.getCurrentAuthenticationManager().setUsePassCode(passcodeUsed) - hasSetupPassword = true - } - - fun hasFinishedSetupPassword() { - tempPassword = null - } - - fun startedInitialSetup() { - authPreferences.setHasCompletedInitialSetup(false) - hasCompletedInitialSetup = false - } - - fun hasCompletedInitialSetup() { - authPreferences.setHasCompletedInitialSetup(true) - hasCompletedInitialSetup = true - } - - fun hasCompletedOnboarding() { - authPreferences.setHasCompletedOnboarding(true) - hasCompleteOnboarding = true - } - - fun setHasShowedInitialAnimation() { - authPreferences.setHasShowedInitialAnimation(true) - } - - fun getHasShowedInitialAnimation(): Boolean { - return authPreferences.getShowedInitialAnimation() - } - - fun startPasswordSetup(password: String) { - tempPassword = password - } - - fun checkPassword(password: String): Boolean { - return password.equals(tempPassword) - } - - fun getPasswordToSetUp(): String? = tempPassword - - fun hasLoggedInUser() { - _isLoggedIn.value = true - resetLogoutTimeout() - } - - fun resetLogoutTimeout() { - if (_isLoggedIn.value!!) { - inactivityCountDownTimer.cancel() - inactivityCountDownTimer.start() - } - } - - var inactivityCountDownTimer = - object : CountDownTimer(60 * 5 * 1000.toLong(), 1000) { - override fun onTick(millisUntilFinished: Long) {} - - override fun onFinish() { - _isLoggedIn.value = false - } - } - - fun getBiometricAuthKeyName(): String { - return authPreferences.getAuthKeyName() - } - - fun setBiometricAuthKeyName(resetBiometricKeyNameAppendix: String) { - authPreferences.setAuthKeyName(resetBiometricKeyNameAppendix) - } - - fun isAccountsBackupPossible(): Boolean { - return !authPreferences.hasEncryptedSeed() - } - - fun isAccountsBackedUp(): Boolean { - return authPreferences.isAccountsBackedUp() - } - - fun setAccountsBackedUp(value: Boolean) { - return authPreferences.setAccountsBackedUp(value) - } - - fun addAccountsBackedUpListener(listener: Preferences.Listener) { - authPreferences.addAccountsBackedUpListener(listener) - } - - fun removeAccountsBackedUpListener(listener: Preferences.Listener) { - authPreferences.removeListener(listener) - } -} diff --git a/app/src/main/java/com/concordium/wallet/core/migration/TwoWalletsMigration.kt b/app/src/main/java/com/concordium/wallet/core/migration/TwoWalletsMigration.kt new file mode 100644 index 00000000..83373725 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/migration/TwoWalletsMigration.kt @@ -0,0 +1,211 @@ +package com.concordium.wallet.core.migration + +import android.content.Context +import android.util.Base64 +import com.concordium.wallet.core.multiwallet.AppWallet +import com.concordium.wallet.core.security.KeystoreHelper +import com.concordium.wallet.data.model.EncryptedData +import com.concordium.wallet.data.preferences.AppSetupPreferences +import com.concordium.wallet.data.preferences.Preferences +import com.concordium.wallet.data.preferences.WalletSetupPreferences +import com.concordium.wallet.data.room.WalletDatabase +import com.concordium.wallet.data.room.app.AppDatabase +import com.concordium.wallet.data.room.app.AppWalletDao +import com.concordium.wallet.data.room.app.AppWalletEntity +import com.google.gson.Gson +import java.io.File + +class TwoWalletsMigration( + private val context: Context, + private val gson: Gson, +) { + private val oldAuthPreferences: OldAuthPreferences by lazy { + OldAuthPreferences(context) + } + + private val oldEncryptionIv: ByteArray? by lazy { + oldAuthPreferences.passwordEncryptionInitVectorBase64 + ?.let(::decodeOldBase64) + } + + private val appWalletDao: AppWalletDao by lazy { + AppDatabase.getDatabase(context).appWalletDao() + } + + fun migrateOldEncryptedData(oldEncryptedDataBase64: String) = + EncryptedData( + ciphertext = decodeOldBase64(oldEncryptedDataBase64), + iv = checkNotNull(oldEncryptionIv) { + "Can't migrate old encrypted data due to the missing old encryption IV" + }, + transformation = OLD_ENCRYPTION_TRANSFORMATION, + ) + + fun isPreferenceMigrationNeeded(): Boolean = + File(context.dataDir, "/shared_prefs/$OLD_AUTH_PREFERENCES_FILE.xml").exists() + && !oldAuthPreferences.areMigrated + + @Suppress("DEPRECATION") + fun migratePreferencesOnce() { + check(isPreferenceMigrationNeeded()) { + "The migration is not needed" + } + + val newAppSetupPreferences = AppSetupPreferences( + context = context, + gson = gson, + ) + val newWalletSetupPreferences = WalletSetupPreferences( + context = context, + gson = gson, + ) + + // Auth. + val authSlot = oldAuthPreferences.authKeyName + newAppSetupPreferences.setCurrentAuthSlot(authSlot) + newAppSetupPreferences.setUsePasscode(authSlot, oldAuthPreferences.usePasscode) + newAppSetupPreferences.setUseBiometrics(authSlot, oldAuthPreferences.useBiometrics) + oldAuthPreferences.passwordEncryptionSaltBase64 + ?.let(::decodeOldBase64) + ?.also { newAppSetupPreferences.setPasswordKeySalt(authSlot, it) } + oldAuthPreferences.encryptedPasswordBase64 + ?.let(::decodeOldBase64) + ?.also { encryptedPassword -> + val encryptedPasswordIv = oldAuthPreferences.encryptedPasswordIvBase64 + ?.let(::decodeOldBase64) + ?: error("Can't save the encrypted password: missing IV") + newAppSetupPreferences.setEncryptedPassword( + authSlot, + EncryptedData( + ciphertext = encryptedPassword, + iv = encryptedPasswordIv, + transformation = KeystoreHelper.ENCRYPTION_CIPHER_TRANSFORMATION, + ) + ) + } + oldAuthPreferences.passwordCheck + ?.also { newAppSetupPreferences.setLegacyPasswordCheck(authSlot, it) } + oldAuthPreferences.encryptedPasswordCheckBase64 + ?.let(::migrateOldEncryptedData) + ?.also { newAppSetupPreferences.setLegacyEncryptedPasswordCheck(authSlot, it) } + + // App setup. + newAppSetupPreferences.setHasCompletedInitialSetup(oldAuthPreferences.hasCompletedInitialSetup) + + // Wallet setup. + newWalletSetupPreferences.setHasCompletedOnboarding(oldAuthPreferences.hasCompletedOnboarding) + newWalletSetupPreferences.setAccountsBackedUp(oldAuthPreferences.areAccountsBackedUp) + newWalletSetupPreferences.setHasShownInitialAnimation(oldAuthPreferences.hasShownInitialAnimation) + oldAuthPreferences.encryptedSeedEntropyHexBase64 + ?.let(::migrateOldEncryptedData) + ?.also { newEncryptedSeedEntropyHex -> + check( + newWalletSetupPreferences.tryToSetEncryptedSeedPhrase(newEncryptedSeedEntropyHex) + ) { "Failed setting encrypted seed phrase" } + } + oldAuthPreferences.encryptedSeedHexBase64 + ?.let(::migrateOldEncryptedData) + ?.also { newEncryptedSeedHex -> + check( + newWalletSetupPreferences.tryToSetEncryptedSeedHex(newEncryptedSeedHex) + ) { "Failed setting encrypted seed" } + } + + // Finalize. + oldAuthPreferences.areMigrated = true + } + + suspend fun isAppDatabaseMigrationNeeded(): Boolean = + // The migration is needed when updated to the "two wallets" release + // as well as when the app is just installed so the DB must be prepared. + appWalletDao.getCount() == 0 + + @Suppress("DEPRECATION") + suspend fun migrateAppDatabaseOnce() { + check(isAppDatabaseMigrationNeeded()) { + "The migration is not needed" + } + + val primaryWalletDatabase = WalletDatabase.getDatabase(context) + val primaryWalletType = when { + primaryWalletDatabase.identityDao().getCount() > 0 + && oldAuthPreferences.encryptedSeedHexBase64 == null + && oldAuthPreferences.encryptedSeedEntropyHexBase64 == null -> + AppWallet.Type.FILE + + else -> + AppWallet.Type.SEED + } + + // Creation of the first (primary) wallet. + appWalletDao.insertAndActivate( + AppWalletEntity( + wallet = AppWallet.primary( + type = primaryWalletType, + ) + ) + ) + } + + private class OldAuthPreferences( + context: Context, + ) : Preferences(context, OLD_AUTH_PREFERENCES_FILE) { + + val authKeyName: String + get() = getString("PREFKEY_BIOMETRIC_KEY", "default_key") + + val passwordEncryptionInitVectorBase64: String? + get() = getString("PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR$authKeyName") + + val passwordEncryptionSaltBase64: String? + get() = getString("PREFKEY_PASSWORD_ENCRYPTION_SALT$authKeyName") + + val usePasscode: Boolean + get() = getBoolean("PREFKEY_USE_PASSCODE$authKeyName", false) + + val useBiometrics: Boolean + get() = getBoolean("PREFKEY_USE_BIOMETRICS$authKeyName", false) + + val passwordCheck: String? + get() = getString("PREFKEY_PASSWORD_CHECK$authKeyName") + + val encryptedPasswordCheckBase64: String? + get() = getString("PREFKEY_PASSWORD_CHECK_ENCRYPTED$authKeyName") + + val encryptedPasswordBase64: String? + get() = getString("PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY$authKeyName") + + val encryptedPasswordIvBase64: String? + get() = getString("PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR$authKeyName") + + val hasCompletedInitialSetup: Boolean + get() = getBoolean("PREFKEY_HAS_COMPLETED_INITIAL_SETUP", true) + + val hasCompletedOnboarding: Boolean + get() = getBoolean("PREFKEY_HAS_COMPLETED_ONBOARDING", false) + + val areAccountsBackedUp: Boolean + get() = getBoolean("PREFKEY_ACCOUNTS_BACKED_UP", true) + + val encryptedSeedEntropyHexBase64: String? + get() = getString("PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX") + + val encryptedSeedHexBase64: String? + get() = getString("SEED_PHRASE_ENCRYPTED") + + val hasShownInitialAnimation: Boolean + get() = getBoolean("PREFKEY_HAS_SHOWED_INITIAL_ANIMATION", false) + + var areMigrated: Boolean + get() = getBoolean("MIGRATED", false) + set(value) = setBoolean("MIGRATED", value) + } + + companion object { + private const val OLD_ENCRYPTION_TRANSFORMATION = "AES/CBC/PKCS7Padding" + private const val OLD_AUTH_PREFERENCES_FILE = "PREF_FILE_AUTH" + + private fun decodeOldBase64(encoded: String): ByteArray = + Base64.decode(encoded, Base64.DEFAULT) + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/AddAndActivateWalletUseCase.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/AddAndActivateWalletUseCase.kt new file mode 100644 index 00000000..3b7c2fbe --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/AddAndActivateWalletUseCase.kt @@ -0,0 +1,29 @@ +package com.concordium.wallet.core.multiwallet + +import com.concordium.wallet.App +import com.concordium.wallet.core.AppCore + +class AddAndActivateWalletUseCase { + /** + * Adds and activates an extra wallet of the given [walletType]. + * Once the wallet is added, a new session is started with it. + * On completion, the main screen must be re-started. + * + * @return ID if the added wallet + * + * @see AppCore.startNewSession + */ + suspend operator fun invoke( + walletType: AppWallet.Type, + ): String { + val newWallet = AppWallet.extra( + type = walletType, + ) + + App.appCore.walletRepository.addWallet(newWallet) + App.appCore.startNewSession( + activeWallet = newWallet, + ) + return newWallet.id + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/AppWallet.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/AppWallet.kt new file mode 100644 index 00000000..cf8f0eba --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/AppWallet.kt @@ -0,0 +1,58 @@ +package com.concordium.wallet.core.multiwallet + +import com.concordium.wallet.data.room.app.AppWalletEntity +import java.util.Date + +class AppWallet +private constructor( + val id: String, + val type: Type, + val createdAt: Date, +) { + constructor(entity: AppWalletEntity) : this( + id = entity.id, + type = Type.valueOf(entity.type), + createdAt = Date(entity.createdAt), + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is AppWallet) return false + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id.hashCode() + } + + enum class Type { + FILE, + SEED, + ; + } + + companion object { + fun primary( + type: Type, + ) = AppWallet( + id = "", + type = type, + createdAt = Date(0), + ) + + fun extra( + type: Type, + ): AppWallet { + val now = Date() + + return AppWallet( + id = now.time.toString(), + type = type, + createdAt = now, + ) + } + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/DeleteActiveWalletUseCase.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/DeleteActiveWalletUseCase.kt new file mode 100644 index 00000000..9e21d8a7 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/DeleteActiveWalletUseCase.kt @@ -0,0 +1,30 @@ +package com.concordium.wallet.core.multiwallet + +import com.concordium.wallet.App + +class DeleteActiveWalletUseCase { + /** + * Deletes the current active wallet and switches it to the [newActiveWallet]. + * Once the [newActiveWallet] is activated, a new session is started with it. + * On completion, the main screen must be re-started. + * + * @see SwitchActiveWalletUseCase + */ + suspend operator fun invoke( + newActiveWallet: AppWallet, + ) { + check(App.appCore.walletRepository.getWallets().size > 1) { + "Can't delete the only wallet" + } + + App.appCore.walletRepository.delete( + walletToDeleteId = App.appCore.session.activeWallet.id, + walletToActivateId = newActiveWallet.id, + ) + App.appCore.session.walletStorage.erase() + + SwitchActiveWalletUseCase().invoke( + newActiveWallet = newActiveWallet, + ) + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletTypeUseCase.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletTypeUseCase.kt new file mode 100644 index 00000000..c1eb669f --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletTypeUseCase.kt @@ -0,0 +1,45 @@ +package com.concordium.wallet.core.multiwallet + +import com.concordium.wallet.App +import com.concordium.wallet.core.AppCore +import com.concordium.wallet.data.AccountRepository +import com.concordium.wallet.data.IdentityRepository +import com.concordium.wallet.util.Log + +class SwitchActiveWalletTypeUseCase { + + /** + * Switches the current active wallet type to the given [newWalletType], + * which is done when setting up the first wallet. + * Once the wallet type is switched, a new session is started with it. + * + * @see AppCore.startNewSession + */ + suspend operator fun invoke( + newWalletType: AppWallet.Type, + ) { + val activeWallet = App.appCore.session.activeWallet + + if (activeWallet.type == newWalletType) { + Log.d("The active wallet is already $newWalletType, skipping") + return + } + + val allAccounts = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()).getAll() + val allIdentities = + IdentityRepository(App.appCore.session.walletStorage.database.identityDao()).getAll() + + check(allAccounts.isEmpty() && allIdentities.isEmpty()) { + "Can't switch the wallet type when it is not empty" + } + + App.appCore.walletRepository.switchWalletType( + walletId = activeWallet.id, + newType = newWalletType, + ) + App.appCore.startNewSession( + activeWallet = App.appCore.walletRepository.getActiveWallet(), + ) + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletUseCase.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletUseCase.kt new file mode 100644 index 00000000..a3fddd3f --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletUseCase.kt @@ -0,0 +1,22 @@ +package com.concordium.wallet.core.multiwallet + +import com.concordium.wallet.App +import com.concordium.wallet.core.AppCore + +class SwitchActiveWalletUseCase { + /** + * Switches the active wallet to the given [newActiveWallet]. + * Once the wallet is activated, a new session is started with it. + * On completion, the main screen must be re-started. + * + * @see AppCore.startNewSession + */ + suspend operator fun invoke( + newActiveWallet: AppWallet, + ) { + App.appCore.walletRepository.activate(newActiveWallet) + App.appCore.startNewSession( + activeWallet = newActiveWallet, + ) + } +} diff --git a/app/src/main/java/com/concordium/wallet/core/notifications/FcmNotificationsService.kt b/app/src/main/java/com/concordium/wallet/core/notifications/FcmNotificationsService.kt index 0ce815e6..262eef8d 100644 --- a/app/src/main/java/com/concordium/wallet/core/notifications/FcmNotificationsService.kt +++ b/app/src/main/java/com/concordium/wallet/core/notifications/FcmNotificationsService.kt @@ -6,7 +6,6 @@ import com.concordium.wallet.data.ContractTokensRepository import com.concordium.wallet.data.cryptolib.ContractAddress import com.concordium.wallet.data.model.Token import com.concordium.wallet.data.room.ContractToken -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.ui.cis2.retrofit.MetadataApiInstance import com.concordium.wallet.util.Log import com.concordium.wallet.util.toBigInteger @@ -20,23 +19,6 @@ import kotlinx.coroutines.runBlocking class FcmNotificationsService : FirebaseMessagingService() { private val serviceCoroutineContext = Dispatchers.IO + SupervisorJob() - private val announcementNotificationManager: AnnouncementNotificationManager by lazy { - AnnouncementNotificationManager(application) - } - private val transactionNotificationsManager: TransactionNotificationsManager by lazy { - TransactionNotificationsManager(application) - } - private val accountRepository: AccountRepository by lazy { - val accountDao = WalletDatabase.getDatabase(application).accountDao() - AccountRepository(accountDao) - } - private val contractTokensRepository: ContractTokensRepository by lazy { - val contractTokenDao = WalletDatabase.getDatabase(application).contractTokenDao() - ContractTokensRepository(contractTokenDao) - } - private val updateNotificationsSubscriptionUseCase by lazy { - UpdateNotificationsSubscriptionUseCase(application) - } override fun onNewToken(token: String) = runBlocking(serviceCoroutineContext) { Log.d( @@ -45,9 +27,9 @@ class FcmNotificationsService : FirebaseMessagingService() { ) try { - updateNotificationsSubscriptionUseCase() + UpdateNotificationsSubscriptionUseCase().invoke() Log.d("trying update subscriptions with new token") - } catch (error: Exception){ + } catch (error: Exception) { Log.e("failed_updating_subscriptions", error) } } @@ -115,7 +97,7 @@ class FcmNotificationsService : FirebaseMessagingService() { "\nmessageId=$messageId" ) - announcementNotificationManager.notifyAnnouncement( + AnnouncementNotificationManager(application).notifyAnnouncement( title = notificationTitle, text = notificationBody, reference = messageId ?: System.currentTimeMillis(), @@ -137,10 +119,12 @@ class FcmNotificationsService : FirebaseMessagingService() { val recipientAccountAddress = data["recipient"] ?: error("Recipient is missing or invalid") - val recipientAccount = accountRepository.findByAddress(recipientAccountAddress) - ?: error("Recipient account not found in the wallet: $recipientAccountAddress") + val recipientAccount = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) + .findByAddress(recipientAccountAddress) + ?: error("Recipient account not found in the wallet: $recipientAccountAddress") - transactionNotificationsManager.notifyCcdTransaction( + TransactionNotificationsManager(application).notifyCcdTransaction( receivedAmount = amount, account = recipientAccount, reference = data["reference"] ?: System.currentTimeMillis(), @@ -162,8 +146,10 @@ class FcmNotificationsService : FirebaseMessagingService() { val recipientAccountAddress = data["recipient"] ?: error("Recipient is missing or invalid") - val recipientAccount = accountRepository.findByAddress(recipientAccountAddress) - ?: error("Recipient account not found in the wallet: $recipientAccountAddress") + val recipientAccount = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) + .findByAddress(recipientAccountAddress) + ?: error("Recipient account not found in the wallet: $recipientAccountAddress") val contractAddress: ContractAddress = data["contract_address"] ?.let { App.appCore.gson.fromJson(it, ContractAddress::class.java) } @@ -177,6 +163,9 @@ class FcmNotificationsService : FirebaseMessagingService() { ?: error("tokenMetadata is missing or invalid") Log.d("tokenMetadata: $tokenMetadata") + val contractTokensRepository = + ContractTokensRepository(App.appCore.session.walletStorage.database.contractTokenDao()) + val existingContractToken = contractTokensRepository.find( accountAddress = recipientAccountAddress, contractIndex = contractAddress.index.toString(), @@ -228,7 +217,7 @@ class FcmNotificationsService : FirebaseMessagingService() { token = Token(existingContractToken) } - transactionNotificationsManager.notifyCis2Transaction( + TransactionNotificationsManager(application).notifyCis2Transaction( receivedAmount = amount, token = token, account = recipientAccount, diff --git a/app/src/main/java/com/concordium/wallet/core/notifications/TransactionNotificationsManager.kt b/app/src/main/java/com/concordium/wallet/core/notifications/TransactionNotificationsManager.kt index a656cd7f..c60f77f6 100644 --- a/app/src/main/java/com/concordium/wallet/core/notifications/TransactionNotificationsManager.kt +++ b/app/src/main/java/com/concordium/wallet/core/notifications/TransactionNotificationsManager.kt @@ -8,9 +8,10 @@ import android.content.Intent import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import com.concordium.wallet.App import com.concordium.wallet.R import com.concordium.wallet.data.model.Token -import com.concordium.wallet.data.preferences.NotificationsPreferences +import com.concordium.wallet.data.preferences.WalletNotificationsPreferences import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.util.CurrencyUtil import com.concordium.wallet.ui.account.accountdetails.AccountDetailsActivity @@ -24,18 +25,18 @@ class TransactionNotificationsManager( NotificationManagerCompat.from(context) } - private val notificationsPreferences: NotificationsPreferences by lazy { - NotificationsPreferences(context) + private val walletNotificationsPreferences: WalletNotificationsPreferences by lazy { + App.appCore.session.walletStorage.notificationsPreferences } private val areNotificationsEnabled: Boolean get() = notificationsManager.areNotificationsEnabled() private val areCcdTxNotificationsEnabled: Boolean - get() = areNotificationsEnabled && notificationsPreferences.areCcdTxNotificationsEnabled + get() = areNotificationsEnabled && walletNotificationsPreferences.areCcdTxNotificationsEnabled private val areCis2TxNotificationsEnabled: Boolean - get() = areNotificationsEnabled && notificationsPreferences.areCis2TxNotificationsEnabled + get() = areNotificationsEnabled && walletNotificationsPreferences.areCis2TxNotificationsEnabled @SuppressLint("MissingPermission") fun notifyCcdTransaction( diff --git a/app/src/main/java/com/concordium/wallet/core/notifications/UpdateNotificationsSubscriptionUseCase.kt b/app/src/main/java/com/concordium/wallet/core/notifications/UpdateNotificationsSubscriptionUseCase.kt index f5abf314..6965e083 100644 --- a/app/src/main/java/com/concordium/wallet/core/notifications/UpdateNotificationsSubscriptionUseCase.kt +++ b/app/src/main/java/com/concordium/wallet/core/notifications/UpdateNotificationsSubscriptionUseCase.kt @@ -1,14 +1,13 @@ package com.concordium.wallet.core.notifications -import android.app.Application +import android.content.Context import com.concordium.wallet.App import com.concordium.wallet.data.AccountRepository import com.concordium.wallet.data.backend.notifications.NotificationsBackend import com.concordium.wallet.data.backend.notifications.UpdateSubscriptionRequest import com.concordium.wallet.data.model.NotificationsTopic -import com.concordium.wallet.data.preferences.NotificationsPreferences +import com.concordium.wallet.data.preferences.WalletNotificationsPreferences import com.concordium.wallet.data.room.Account -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.util.Log import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability @@ -19,27 +18,26 @@ import java.net.HttpURLConnection class UpdateNotificationsSubscriptionUseCase( private val accountRepository: AccountRepository, - private val notificationsPreferences: NotificationsPreferences, + private val walletNotificationsPreferences: WalletNotificationsPreferences, private val notificationsBackend: NotificationsBackend, - private val application: Application, + private val context: Context, ) { - constructor(application: Application) : this( - accountRepository = WalletDatabase.getDatabase(application).accountDao() - .let(::AccountRepository), - notificationsPreferences = NotificationsPreferences(application), + constructor() : this( + accountRepository = AccountRepository(App.appCore.session.walletStorage.database.accountDao()), + walletNotificationsPreferences = App.appCore.session.walletStorage.notificationsPreferences, notificationsBackend = App.appCore.getNotificationsBackend(), - application = application + context = App.appContext, ) /** * @return **true** on successful update */ suspend operator fun invoke( - isCcdTxEnabled: Boolean = notificationsPreferences.areCcdTxNotificationsEnabled, - isCis2TxEnabled: Boolean = notificationsPreferences.areCis2TxNotificationsEnabled, + isCcdTxEnabled: Boolean = walletNotificationsPreferences.areCcdTxNotificationsEnabled, + isCis2TxEnabled: Boolean = walletNotificationsPreferences.areCis2TxNotificationsEnabled ): Boolean { val googleApiAvailability = GoogleApiAvailability.getInstance() - val fcmToken = when (googleApiAvailability.isGooglePlayServicesAvailable(application)) { + val fcmToken = when (googleApiAvailability.isGooglePlayServicesAvailable(context)) { ConnectionResult.SUCCESS -> { try { FirebaseMessaging.getInstance().token.await() diff --git a/app/src/main/java/com/concordium/wallet/core/security/EncryptionException.kt b/app/src/main/java/com/concordium/wallet/core/security/EncryptionException.kt index 17606ca3..554a9464 100644 --- a/app/src/main/java/com/concordium/wallet/core/security/EncryptionException.kt +++ b/app/src/main/java/com/concordium/wallet/core/security/EncryptionException.kt @@ -1,7 +1,3 @@ package com.concordium.wallet.core.security -class EncryptionException : Exception { - constructor(message: String?) : super(message) - constructor(cause: Throwable?) : super(cause) - constructor(message: String?, cause: Throwable?) : super(message, cause) -} \ No newline at end of file +class EncryptionException(cause: Throwable?) : Exception(cause) diff --git a/app/src/main/java/com/concordium/wallet/core/security/EncryptionHelper.kt b/app/src/main/java/com/concordium/wallet/core/security/EncryptionHelper.kt index 3c67bac2..9d62d4e3 100644 --- a/app/src/main/java/com/concordium/wallet/core/security/EncryptionHelper.kt +++ b/app/src/main/java/com/concordium/wallet/core/security/EncryptionHelper.kt @@ -1,122 +1,51 @@ package com.concordium.wallet.core.security import android.security.keystore.KeyProperties -import android.util.Base64 +import com.concordium.wallet.data.model.EncryptedData import com.concordium.wallet.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.UnsupportedEncodingException import java.security.InvalidAlgorithmParameterException import java.security.InvalidKeyException import java.security.NoSuchAlgorithmException import java.security.SecureRandom -import java.security.spec.InvalidKeySpecException -import java.security.spec.KeySpec import javax.crypto.* import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec /** - * Encryption functions. Beware that these are used for both authentication and export. + * Encryption and key derivation functions. */ object EncryptionHelper { + private const val KDF = "PBKDF2WithHmacSHA256" + private const val KDF_ITERATION_COUNT = 10000 - private val t = this.javaClass.simpleName - - private const val DEFAULT_ITERATION_COUNT = 10000 - private const val KEY_LENGTH = 256 - private const val CIPHER_TRANSFORMATION = - "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/PKCS7Padding" - - /** - * @exception EncryptionException - */ - fun createEncryptionData(): Pair { - val saltLength = KEY_LENGTH / 8 // same size as key output - val random = SecureRandom() - val salt = ByteArray(saltLength) - random.nextBytes(salt) - - try { - val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION) - val iv = ByteArray(cipher.blockSize) - random.nextBytes(iv) - - return Pair(salt, iv) - } catch (e: Exception) { - when (e) { - is NoSuchAlgorithmException, - is NoSuchPaddingException -> { - Log.d("$t: Failed creating encryption data", e) - throw EncryptionException(e) - } - else -> throw e - } - } - } + private const val ENCRYPTION_KEY_SIZE_BITS = 256 + private const val ENCRYPTION_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + private const val ENCRYPTION_CIPHER_TRANSFORMATION = + "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_GCM}/NoPadding" /** - * @exception EncryptionException + * Encrypts given [data] with the given [key] using the best suitable cipher. + * + * @return [EncryptedData] with a unique IV. */ @Throws(EncryptionException::class) - fun generateKey( - password: String, - salt: ByteArray, - iterationCount: Int = DEFAULT_ITERATION_COUNT - ): SecretKey { - Log.i("$t: generateKey. Params --> password: $password, salt: $salt") - val keyBytes = generateKeyAsByteArray(password, salt, iterationCount) - val key: SecretKey = SecretKeySpec(keyBytes, "AES") - return key - } - - /** - * @exception EncryptionException - */ - private fun generateKeyAsByteArray( - password: String, - salt: ByteArray, - iterationCount: Int = DEFAULT_ITERATION_COUNT - ): ByteArray { + suspend fun encrypt( + key: ByteArray, + data: ByteArray, + cipherTransformation: String = ENCRYPTION_CIPHER_TRANSFORMATION, + ): EncryptedData = withContext(Dispatchers.Default) { try { - val keySpec: KeySpec = - PBEKeySpec( - password.toCharArray(), salt, - iterationCount, - KEY_LENGTH - ) - val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") - val keyBytes = keyFactory.generateSecret(keySpec).encoded - return keyBytes - } catch (e: Exception) { - when (e) { - is NoSuchAlgorithmException, - is InvalidKeySpecException -> { - Log.d("Failed to create key", e) - throw EncryptionException(e) - } - else -> throw e - } - } - } - - /** - * @exception EncryptionException - */ - fun encrypt( - key: SecretKey, - iv: ByteArray, - toBeEncrypted: String, - base64EncodeFlags: Int = Base64.DEFAULT - ): String { - try { - val toBeEncryptedByteArray = toBeEncrypted.toByteArray(charset("UTF-8")) - val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION) - val ivParams = IvParameterSpec(iv) - cipher.init(Cipher.ENCRYPT_MODE, key, ivParams) - val cipherText = cipher.doFinal(toBeEncryptedByteArray) - val encodedEncrypted = Base64.encodeToString(cipherText, base64EncodeFlags) - Log.d("Encrypted text: $encodedEncrypted") - return encodedEncrypted + val cipher = Cipher.getInstance(cipherTransformation) + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, cipher.algorithm)) + EncryptedData( + ciphertext = cipher.doFinal(data), + transformation = cipher.algorithm, + iv = cipher.iv, + ) } catch (e: Exception) { when (e) { is NoSuchAlgorithmException, @@ -129,27 +58,25 @@ object EncryptionHelper { Log.d("Failed to encrypt data", e) throw EncryptionException(e) } + else -> throw e } } } - /** - * @exception EncryptionException - */ - fun decrypt( - key: SecretKey, - iv: ByteArray, - toBeDecrypted: ByteArray - ): String { + @Throws(EncryptionException::class) + suspend fun decrypt( + key: ByteArray, + encryptedData: EncryptedData, + ): ByteArray = withContext(Dispatchers.Default) { try { - val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION) - val ivParams = IvParameterSpec(iv) - cipher.init(Cipher.DECRYPT_MODE, key, ivParams) - val plainText = cipher.doFinal(toBeDecrypted) - val decryptedString = String(plainText, charset("UTF-8")) - Log.d("Decrypted text: $decryptedString") - return decryptedString + val cipher = Cipher.getInstance(encryptedData.transformation) + cipher.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(key, cipher.algorithm), + IvParameterSpec(encryptedData.decodeIv()) + ) + cipher.doFinal(encryptedData.decodeCiphertext()) } catch (e: Exception) { when (e) { is NoSuchAlgorithmException, @@ -162,35 +89,56 @@ object EncryptionHelper { Log.d("Failed to decrypt data", e) throw EncryptionException(e) } + else -> throw e } } } - /** - * @exception EncryptionException - */ - fun encrypt( - password: String, - salt: ByteArray, - iv: ByteArray, - toBeEncrypted: String, - base64EncodeFlags: Int = Base64.DEFAULT - ): String { - val key = generateKey(password, salt) - return encrypt(key, iv, toBeEncrypted, base64EncodeFlags) + @Throws(EncryptionException::class) + suspend fun generateKey(): ByteArray = withContext(Dispatchers.Default) { + try { + val generator = KeyGenerator.getInstance(ENCRYPTION_KEY_ALGORITHM) + generator.init(ENCRYPTION_KEY_SIZE_BITS) + generator.generateKey().encoded + } catch (e: Exception) { + Log.d("Failed to generate a key", e) + throw EncryptionException(e) + } } - /** - * @exception EncryptionException - */ - fun decrypt( - password: String, + @Throws(EncryptionException::class) + suspend fun generatePasswordKeySalt(): ByteArray = withContext(Dispatchers.Default) { + try { + SecureRandom().generateSeed(ENCRYPTION_KEY_SIZE_BITS / 8) + } catch (e: Exception) { + Log.d("Failed to generate password key salt", e) + throw EncryptionException(e) + } + } + + @Throws(EncryptionException::class) + suspend fun generatePasswordKey( + password: CharArray, salt: ByteArray, - iv: ByteArray, - toBeDecrypted: ByteArray - ): String { - val key = generateKey(password, salt) - return decrypt(key, iv, toBeDecrypted) + sizeBits: Int = ENCRYPTION_KEY_SIZE_BITS, + kdf: String = KDF, + kdfIterationCount: Int = KDF_ITERATION_COUNT, + ): ByteArray = withContext(Dispatchers.Default) { + try { + SecretKeyFactory.getInstance(kdf) + .generateSecret( + PBEKeySpec( + password, + salt, + kdfIterationCount, + sizeBits, + ) + ) + .encoded + } catch (e: Exception) { + Log.d("Failed to generate password key", e) + throw EncryptionException(e) + } } } diff --git a/app/src/main/java/com/concordium/wallet/core/security/KeystoreHelper.kt b/app/src/main/java/com/concordium/wallet/core/security/KeystoreHelper.kt index 52df6fbc..d5334059 100644 --- a/app/src/main/java/com/concordium/wallet/core/security/KeystoreHelper.kt +++ b/app/src/main/java/com/concordium/wallet/core/security/KeystoreHelper.kt @@ -5,7 +5,13 @@ import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyProperties import com.concordium.wallet.util.Log import java.io.IOException -import java.security.* +import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.NoSuchProviderException +import java.security.UnrecoverableKeyException import java.security.cert.CertificateException import javax.crypto.Cipher import javax.crypto.KeyGenerator @@ -17,11 +23,15 @@ class KeystoreHelper { companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" + private const val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + private const val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC + private const val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 + const val ENCRYPTION_CIPHER_TRANSFORMATION = + "$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING" } private fun generateSecretKeyWithSpecs(keyGenParameterSpec: KeyGenParameterSpec) { - val keyGenerator = - KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE) + val keyGenerator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, ANDROID_KEY_STORE) keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey() } @@ -30,9 +40,9 @@ class KeystoreHelper { try { val keyProperties = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT val builder = KeyGenParameterSpec.Builder(keyName, keyProperties) - .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setBlockModes(ENCRYPTION_BLOCK_MODE) .setUserAuthenticationRequired(true) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .setEncryptionPaddings(ENCRYPTION_PADDING) .setInvalidatedByBiometricEnrollment(true) generateSecretKeyWithSpecs(builder.build()) @@ -42,20 +52,19 @@ class KeystoreHelper { is NoSuchProviderException, is InvalidAlgorithmParameterException, is CertificateException, - is IOException -> { + is IOException, + -> { Log.d("Failed to generate secret key", e) throw KeystoreEncryptionException("Failed to generate secret key", e) } + else -> throw e } } } - private fun setupCipher(): Cipher { - val cipherString = - "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}" - return Cipher.getInstance(cipherString) - } + private fun getCipher(): Cipher = + Cipher.getInstance(ENCRYPTION_CIPHER_TRANSFORMATION) private fun getSecretKey(keyName: String): SecretKey { val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE) @@ -65,7 +74,7 @@ class KeystoreHelper { fun initCipherForEncryption(keyName: String): Cipher? { try { - val cipher = setupCipher() + val cipher = getCipher() val secretKey = getSecretKey(keyName) cipher.init(Cipher.ENCRYPT_MODE, secretKey) return cipher @@ -78,10 +87,12 @@ class KeystoreHelper { is IOException, is NoSuchAlgorithmException, is NoSuchPaddingException, - is InvalidKeyException -> { + is InvalidKeyException, + -> { Log.d("Failed to init Cipher", e) throw KeystoreEncryptionException("Failed to init Cipher", e) } + else -> throw e } } @@ -89,7 +100,7 @@ class KeystoreHelper { fun initCipherForDecryption(keyName: String, initVector: ByteArray): Cipher? { try { - val cipher = setupCipher() + val cipher = getCipher() val secretKey = getSecretKey(keyName) val ivParams = IvParameterSpec(initVector) cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParams) @@ -104,12 +115,14 @@ class KeystoreHelper { is NoSuchAlgorithmException, is NoSuchPaddingException, is InvalidAlgorithmParameterException, - is InvalidKeyException -> { + is InvalidKeyException, + -> { Log.d("Failed to init Cipher", e) throw KeystoreEncryptionException("Failed to init Cipher", e) } + else -> throw e } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/concordium/wallet/data/AppWalletRepository.kt b/app/src/main/java/com/concordium/wallet/data/AppWalletRepository.kt new file mode 100644 index 00000000..a9b86ec1 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/AppWalletRepository.kt @@ -0,0 +1,59 @@ +package com.concordium.wallet.data + +import com.concordium.wallet.core.multiwallet.AppWallet +import com.concordium.wallet.data.room.app.AppWalletDao +import com.concordium.wallet.data.room.app.AppWalletEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +class AppWalletRepository( + private val appWalletDao: AppWalletDao, +) { + // The first (primary) wallet is created by the Two wallets migration. + + suspend fun getActiveWallet(): AppWallet = + appWalletDao.getActive() + .let(::AppWallet) + + fun getWalletsFlow(): Flow> = + appWalletDao.getAll() + .map { rows -> + rows.map(::AppWallet) + } + + suspend fun getWallets(): List = + getWalletsFlow().first() + + suspend fun addWallet(wallet: AppWallet) { + appWalletDao.insertAndActivate(AppWalletEntity(wallet)) + } + + suspend fun switchWalletType( + walletId: String, + newType: AppWallet.Type + ) { + appWalletDao.switchType( + walletId = walletId, + newType = newType.name, + ) + } + + suspend fun activate( + newActiveWallet: AppWallet, + ) { + appWalletDao.activate( + walletId = newActiveWallet.id, + ) + } + + suspend fun delete( + walletToDeleteId: String, + walletToActivateId: String, + ) { + appWalletDao.deleteAndActivateAnother( + walletToDeleteId = walletToDeleteId, + walletToActivateId = walletToActivateId, + ) + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/WalletStorage.kt b/app/src/main/java/com/concordium/wallet/data/WalletStorage.kt new file mode 100644 index 00000000..c76e1bfd --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/WalletStorage.kt @@ -0,0 +1,79 @@ +package com.concordium.wallet.data + +import android.content.Context +import com.concordium.wallet.core.multiwallet.AppWallet +import com.concordium.wallet.data.preferences.Preferences +import com.concordium.wallet.data.preferences.WalletFilterPreferences +import com.concordium.wallet.data.preferences.WalletIdentityCreationDataPreferences +import com.concordium.wallet.data.preferences.WalletNotificationsPreferences +import com.concordium.wallet.data.preferences.WalletProviderPreferences +import com.concordium.wallet.data.preferences.WalletSendFundsPreferences +import com.concordium.wallet.data.preferences.WalletSetupPreferences +import com.concordium.wallet.data.room.WalletDatabase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * A provider for all the persistence for a single wallet. + * + * @param fileNameSuffix a part to append to default persistence file names. + * For the primary wallet, use an empty string to keep backward compatibility. + */ +@Suppress("DEPRECATION") +class WalletStorage( + private val context: Context, + private val fileNameSuffix: String, +) { + constructor( + context: Context, + activeWallet: AppWallet, + ) : this( + context = context, + fileNameSuffix = + if (activeWallet.id.isEmpty()) + "" + else + "_${activeWallet.id}" + ) + + val database: WalletDatabase by lazy { + WalletDatabase.getDatabase(context, fileNameSuffix) + } + + val filterPreferences: WalletFilterPreferences by lazy { + WalletFilterPreferences(context, fileNameSuffix) + } + + val sendFundsPreferences: WalletSendFundsPreferences by lazy { + WalletSendFundsPreferences(context, fileNameSuffix) + } + + val identityCreationDataPreferences: WalletIdentityCreationDataPreferences by lazy { + WalletIdentityCreationDataPreferences(context, fileNameSuffix) + } + + val notificationsPreferences: WalletNotificationsPreferences by lazy { + WalletNotificationsPreferences(context, fileNameSuffix) + } + + val providerPreferences: WalletProviderPreferences by lazy { + WalletProviderPreferences(context, fileNameSuffix) + } + + val setupPreferences: WalletSetupPreferences by lazy { + WalletSetupPreferences(context, fileNameSuffix) + } + + suspend fun erase() = withContext(Dispatchers.IO) { + database.clearAllTables() + + arrayOf( + filterPreferences, + sendFundsPreferences, + identityCreationDataPreferences, + notificationsPreferences, + providerPreferences, + setupPreferences, + ).forEach(Preferences::clearAll) + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/backend/ProxyBackend.kt b/app/src/main/java/com/concordium/wallet/data/backend/ProxyBackend.kt index ac5b608b..bfe8c679 100644 --- a/app/src/main/java/com/concordium/wallet/data/backend/ProxyBackend.kt +++ b/app/src/main/java/com/concordium/wallet/data/backend/ProxyBackend.kt @@ -101,10 +101,6 @@ interface ProxyBackend { @PUT("v0/testnetGTUDrop/{accountAddress}") fun requestGTUDrop(@Path("accountAddress") accountAddress: String): Call - // Identity Provider - @GET("v0/ip_info") - fun getIdentityProviderInfo(): Call> - @GET("v2/ip_info") fun getV2IdentityProviderInfo(): Call> diff --git a/app/src/main/java/com/concordium/wallet/data/backend/repository/IdentityProviderRepository.kt b/app/src/main/java/com/concordium/wallet/data/backend/repository/IdentityProviderRepository.kt index fade8722..2c195704 100644 --- a/app/src/main/java/com/concordium/wallet/data/backend/repository/IdentityProviderRepository.kt +++ b/app/src/main/java/com/concordium/wallet/data/backend/repository/IdentityProviderRepository.kt @@ -14,16 +14,11 @@ class IdentityProviderRepository { private val backend = App.appCore.getProxyBackend() fun getIdentityProviderInfo( - useLegacy: Boolean, success: (ArrayList) -> Unit, failure: ((Throwable) -> Unit)? ): BackendRequest> { - val call = - if (useLegacy) - backend.getIdentityProviderInfo() - else - backend.getV2IdentityProviderInfo() - call.enqueue(object : BackendCallback>() { + val call = backend.getV2IdentityProviderInfo() + backend.getV2IdentityProviderInfo().enqueue(object : BackendCallback>() { override fun onResponseData(response: ArrayList) { success(response) diff --git a/app/src/main/java/com/concordium/wallet/data/model/EncryptedData.kt b/app/src/main/java/com/concordium/wallet/data/model/EncryptedData.kt new file mode 100644 index 00000000..39e7ef17 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/model/EncryptedData.kt @@ -0,0 +1,62 @@ +package com.concordium.wallet.data.model + +import android.util.Base64 +import com.google.gson.annotations.SerializedName +import java.io.Serializable + +/** + * An encrypted data container. + */ +class EncryptedData( + /** + * Base64-encoded ciphertext. + * + * @see decodeCiphertext + */ + @SerializedName("ct") + val ciphertext: String, + + /** + * Java-style cipher transformation, e.g. "AES/CBC/PKCS7Padding". + */ + @SerializedName("t") + val transformation: String, + + /** + * Base64-encoded cipher initialization vector. + * + * @see decodeIv + */ + @SerializedName("iv") + val iv: String, +): Serializable { + + /** + * @param ciphertext raw ciphertext + * @param transformation Java-style cipher transformation, e.g. "AES/CBC/PKCS7Padding" + * @param iv raw cipher initialization vector + */ + constructor( + ciphertext: ByteArray, + transformation: String, + iv: ByteArray, + ) : this( + ciphertext = Base64.encodeToString(ciphertext, BASE64_FLAGS), + transformation = transformation, + iv = Base64.encodeToString(iv, BASE64_FLAGS) + ) + + fun decodeCiphertext(): ByteArray = + Base64.decode(ciphertext, BASE64_FLAGS) + + fun decodeIv(): ByteArray = + Base64.decode(iv, BASE64_FLAGS) + + override fun toString(): String { + return "EncryptedData(ciphertext='$ciphertext', transformation='$transformation', iv='$iv')" + } + + private companion object { + private const val BASE64_FLAGS = Base64.NO_WRAP or Base64.NO_PADDING + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/model/IdentityCreationData.kt b/app/src/main/java/com/concordium/wallet/data/model/IdentityCreationData.kt index b3964afe..b337ecd4 100644 --- a/app/src/main/java/com/concordium/wallet/data/model/IdentityCreationData.kt +++ b/app/src/main/java/com/concordium/wallet/data/model/IdentityCreationData.kt @@ -4,38 +4,10 @@ import com.concordium.wallet.core.gson.RawJsonTypeAdapter import com.google.gson.annotations.JsonAdapter import java.io.Serializable -sealed class IdentityCreationData( +class IdentityCreationData( val identityProvider: IdentityProvider, @JsonAdapter(RawJsonTypeAdapter::class) val idObjectRequest: RawJson, val identityName: String, val identityIndex: Int, -) : Serializable { - class V0( - val privateIdObjectDataEncrypted: String, - val accountName: String, - val encryptedAccountData: String, - val accountAddress: String, - identityProvider: IdentityProvider, - idObjectRequest: RawJson, - identityName: String, - identityIndex: Int - ) : IdentityCreationData( - identityProvider, - idObjectRequest, - identityName, - identityIndex, - ) - - class V1( - identityProvider: IdentityProvider, - idObjectRequest: RawJson, - identityName: String, - identityIndex: Int, - ) : IdentityCreationData( - identityProvider, - idObjectRequest, - identityName, - identityIndex, - ) -} +): Serializable diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/AppSetupPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/AppSetupPreferences.kt new file mode 100644 index 00000000..fc9b854d --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/preferences/AppSetupPreferences.kt @@ -0,0 +1,109 @@ +package com.concordium.wallet.data.preferences + +import android.content.Context +import com.concordium.wallet.data.model.EncryptedData +import com.concordium.wallet.util.toHex +import com.google.gson.Gson +import okio.ByteString.Companion.decodeHex + +class AppSetupPreferences( + context: Context, + private val gson: Gson, +) : Preferences(context, SharedPreferenceFiles.APP_SETUP.key) { + + fun setUsePasscode(slot: String, value: Boolean) { + setBoolean(PREFKEY_USE_PASSCODE + slot, value) + } + + fun getUsePasscode(slot: String): Boolean { + return getBoolean(PREFKEY_USE_PASSCODE + slot) + } + + fun setUseBiometrics(slot: String, value: Boolean) { + setBoolean(PREFKEY_USE_BIOMETRICS + slot, value) + } + + fun getUseBiometrics(slot: String): Boolean { + return getBoolean(PREFKEY_USE_BIOMETRICS + slot) + } + + fun setPasswordKeySalt(slot: String, value: ByteArray) { + setString(PREFKEY_PASSWORD_KEY_SALT_HEX + slot, value.toHex()) + } + + fun getPasswordKeySalt(slot: String): ByteArray { + return getString(PREFKEY_PASSWORD_KEY_SALT_HEX + slot, "").decodeHex().toByteArray() + } + + fun hasPasswordKeySalt(slot: String): Boolean { + return getString(PREFKEY_PASSWORD_KEY_SALT_HEX + slot) != null + } + + fun setEncryptedPassword(slot: String, value: EncryptedData) { + setJsonSerialized(PREFKEY_ENCRYPTED_PASSWORD_JSON + slot, value, gson) + } + + fun getEncryptedPassword(slot: String): EncryptedData { + return getJsonSerialized(PREFKEY_ENCRYPTED_PASSWORD_JSON + slot, gson)!! + } + + fun setEncryptedMasterKey(slot: String, value: EncryptedData) { + setJsonSerialized(PREFKEY_ENCRYPTED_MASTER_KEY_JSON + slot, value, gson) + } + + fun getEncryptedMasterKey(slot: String): EncryptedData { + return getJsonSerialized(PREFKEY_ENCRYPTED_MASTER_KEY_JSON + slot, gson)!! + } + + fun hasEncryptedMasterKey(slot: String): Boolean { + return getString(PREFKEY_ENCRYPTED_MASTER_KEY_JSON + slot) != null + } + + fun setLegacyEncryptedPasswordCheck(slot: String, value: EncryptedData) { + setJsonSerialized(PREFKEY_LEGACY_ENCRYPTED_PASSWORD_CHECK_JSON + slot, value, gson) + } + + fun getLegacyEncryptedPasswordCheck(slot: String): EncryptedData? { + return getJsonSerialized( + PREFKEY_LEGACY_ENCRYPTED_PASSWORD_CHECK_JSON + slot, + gson + ) + } + + fun setLegacyPasswordCheck(slot: String, value: String) { + setString(PREFKEY_LEGACY_PASSWORD_CHECK + slot, value) + } + + fun getLegacyPasswordCheck(slot: String): String? { + return getString(PREFKEY_LEGACY_PASSWORD_CHECK + slot) + } + + fun getCurrentAuthSlot(): String { + return getString(PREFKEY_CURRENT_AUTH_SLOT, "default_key") + } + + fun setCurrentAuthSlot(slot: String) { + return setString(PREFKEY_CURRENT_AUTH_SLOT, slot) + } + + fun setHasCompletedInitialSetup(value: Boolean) { + setBoolean(PREFKEY_HAS_COMPLETED_INITIAL_SETUP, value) + } + + fun getHasCompletedInitialSetup(): Boolean { + return getBoolean(PREFKEY_HAS_COMPLETED_INITIAL_SETUP, false) + } + + private companion object { + const val PREFKEY_USE_PASSCODE = "PREFKEY_USE_PASSCODE" + const val PREFKEY_USE_BIOMETRICS = "PREFKEY_USE_BIOMETRICS" + const val PREFKEY_PASSWORD_KEY_SALT_HEX = "PREFKEY_PASSWORD_KEY_SALT" + const val PREFKEY_ENCRYPTED_PASSWORD_JSON = "PREFKEY_ENCRYPTED_PASSWORD_JSON" + const val PREFKEY_CURRENT_AUTH_SLOT = "PREFKEY_CURRENT_AUTH_SLOT" + const val PREFKEY_ENCRYPTED_MASTER_KEY_JSON = "PREFKEY_ENCRYPTED_MASTER_KEY_JSON" + const val PREFKEY_HAS_COMPLETED_INITIAL_SETUP = "PREFKEY_HAS_COMPLETED_INITIAL_SETUP" + const val PREFKEY_LEGACY_PASSWORD_CHECK = "PREFKEY_LEGACY_PASSWORD_CHECK" + const val PREFKEY_LEGACY_ENCRYPTED_PASSWORD_CHECK_JSON = + "PREFKEY_LEGACY_ENCRYPTED_PASSWORD_CHECK_JSON" + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/TrackingPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/AppTrackingPreferences.kt similarity index 78% rename from app/src/main/java/com/concordium/wallet/data/preferences/TrackingPreferences.kt rename to app/src/main/java/com/concordium/wallet/data/preferences/AppTrackingPreferences.kt index 6492e170..c965e2bd 100644 --- a/app/src/main/java/com/concordium/wallet/data/preferences/TrackingPreferences.kt +++ b/app/src/main/java/com/concordium/wallet/data/preferences/AppTrackingPreferences.kt @@ -2,8 +2,9 @@ package com.concordium.wallet.data.preferences import android.content.Context -class TrackingPreferences(context: Context) : - Preferences(context, SharedPreferencesKeys.PREF_TRACKING.key, Context.MODE_PRIVATE) { +class AppTrackingPreferences( + context: Context, +) : Preferences(context, SharedPreferenceFiles.APP_TRACKING.key) { var isTrackingEnabled: Boolean by BooleanPreference(PREFKEY_TRACKING_ENABLED, false) diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/AuthPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/AuthPreferences.kt deleted file mode 100644 index 5824bfbf..00000000 --- a/app/src/main/java/com/concordium/wallet/data/preferences/AuthPreferences.kt +++ /dev/null @@ -1,267 +0,0 @@ -package com.concordium.wallet.data.preferences - -import android.content.Context -import cash.z.ecc.android.bip39.Mnemonics -import cash.z.ecc.android.bip39.toSeed -import com.concordium.wallet.App -import com.concordium.wallet.util.toHex -import com.reown.util.hexToBytes -import javax.crypto.SecretKey - -class AuthPreferences(val context: Context) : - Preferences(context, SharedPreferencesKeys.PREF_FILE_AUTH.key, Context.MODE_PRIVATE) { - - companion object { - const val PREFKEY_HAS_SETUP_USER = "PREFKEY_HAS_SETUP_USER" - const val PREFKEY_HAS_COMPLETED_INITIAL_SETUP = "PREFKEY_HAS_COMPLETED_INITIAL_SETUP" - const val PREFKEY_HAS_COMPLETED_ONBOARDING = "PREFKEY_HAS_COMPLETED_ONBOARDING" - const val PREFKEY_USE_PASSCODE = "PREFKEY_USE_PASSCODE" - const val PREFKEY_USE_BIOMETRICS = "PREFKEY_USE_BIOMETRICS" - const val PREFKEY_PASSWORD_CHECK = "PREFKEY_PASSWORD_CHECK" - const val PREFKEY_PASSWORD_CHECK_ENCRYPTED = "PREFKEY_PASSWORD_CHECK_ENCRYPTED" - const val PREFKEY_PASSWORD_ENCRYPTION_SALT = "PREFKEY_PASSWORD_ENCRYPTION_SALT" - const val PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR = "PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR" - const val PREFKEY_ENCRYPTED_PASSWORD = "PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY" - const val PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR = - "PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR" - const val PREFKEY_BIOMETRIC_KEY = "PREFKEY_BIOMETRIC_KEY" - const val PREFKEY_ACCOUNTS_BACKED_UP = "PREFKEY_ACCOUNTS_BACKED_UP" - const val PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX = - "PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX" - const val PREFKEY_LEGACY_SEED_HEX_ENCRYPTED = "SEED_PHRASE_ENCRYPTED" - const val PREFKEY_HAS_SHOWED_INITIAL_ANIMATION = "PREFKEY_HAS_SHOWED_INITIAL_ANIMATION" - } - - fun setHasSetupUser(value: Boolean) { - setBoolean(PREFKEY_HAS_SETUP_USER, value) - } - - fun getHasSetupUser(): Boolean { - return getBoolean(PREFKEY_HAS_SETUP_USER) - } - - fun setHasCompletedInitialSetup(value: Boolean) { - setBoolean(PREFKEY_HAS_COMPLETED_INITIAL_SETUP, value) - } - - fun getHasCompletedInitialSetup(): Boolean { - // Default value is true for backward compatibility. - return getBoolean(PREFKEY_HAS_COMPLETED_INITIAL_SETUP, true) - } - - fun setHasCompletedOnboarding(value: Boolean) { - setBoolean(PREFKEY_HAS_COMPLETED_ONBOARDING, value) - } - - fun getHasCompletedOnboarding(): Boolean { - return getBoolean(PREFKEY_HAS_COMPLETED_ONBOARDING, false) - } - - fun setHasShowedInitialAnimation(value: Boolean) { - setBoolean(PREFKEY_HAS_SHOWED_INITIAL_ANIMATION, value) - } - - fun getShowedInitialAnimation(): Boolean { - return getBoolean(PREFKEY_HAS_SHOWED_INITIAL_ANIMATION, false) - } - - fun setUsePasscode(appendix: String, value: Boolean) { - setBoolean(PREFKEY_USE_PASSCODE + appendix, value) - } - - fun getUsePasscode(appendix: String): Boolean { - return getBoolean(PREFKEY_USE_PASSCODE + appendix) - } - - fun setUseBiometrics(appendix: String, value: Boolean) { - setBoolean(PREFKEY_USE_BIOMETRICS + appendix, value) - } - - fun getUseBiometrics(appendix: String): Boolean { - return getBoolean(PREFKEY_USE_BIOMETRICS + appendix) - } - - fun setPasswordCheck(appendix: String, value: String) { - setString(PREFKEY_PASSWORD_CHECK + appendix, value) - } - - fun getPasswordCheck(appendix: String): String? { - return getString(PREFKEY_PASSWORD_CHECK + appendix) - } - - fun setPasswordCheckEncrypted(appendix: String, value: String) { - setString(PREFKEY_PASSWORD_CHECK_ENCRYPTED + appendix, value) - } - - fun getPasswordCheckEncrypted(appendix: String): String { - return getString(PREFKEY_PASSWORD_CHECK_ENCRYPTED + appendix, "") - } - - fun setPasswordEncryptionSalt(appendix: String, value: String) { - setString(PREFKEY_PASSWORD_ENCRYPTION_SALT + appendix, value) - } - - fun getPasswordEncryptionSalt(appendix: String): String { - return getString(PREFKEY_PASSWORD_ENCRYPTION_SALT + appendix, "") - } - - fun setPasswordEncryptionInitVector(appendix: String, value: String) { - setString(PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR + appendix, value) - } - - fun getPasswordEncryptionInitVector(appendix: String): String { - return getString(PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR + appendix, "") - } - - fun setEncryptedPassword(appendix: String, value: String) { - setString(PREFKEY_ENCRYPTED_PASSWORD + appendix, value) - } - - fun getEncryptedPassword(appendix: String): String { - return getString(PREFKEY_ENCRYPTED_PASSWORD + appendix, "") - } - - fun setEncryptedPasswordDerivedKeyInitVector(appendix: String, value: String) { - setString(PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR + appendix, value) - } - - fun getBiometricsKeyEncryptionInitVector(appendix: String): String { - return getString(PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR + appendix, "") - } - - fun getAuthKeyName(): String { - return getString(PREFKEY_BIOMETRIC_KEY, "default_key") - } - - fun setAuthKeyName(key: String) { - return setString(PREFKEY_BIOMETRIC_KEY, key) - } - - fun isAccountsBackedUp(): Boolean { - return getBoolean(PREFKEY_ACCOUNTS_BACKED_UP, true) - } - - fun setAccountsBackedUp(value: Boolean) { - return setBoolean(PREFKEY_ACCOUNTS_BACKED_UP, value) - } - - fun addAccountsBackedUpListener(listener: Listener) { - addListener(PREFKEY_ACCOUNTS_BACKED_UP, listener) - } - - /** - * Saves the seed phrase as its entropy, so it is possible to later get - * both the seed hex and the seed phrase. - * - * @see getSeedHex - * @see getSeedPhrase - */ - suspend fun tryToSetEncryptedSeedPhrase(seedPhraseString: String, password: String): Boolean { - val entropyHex = Mnemonics.MnemonicCode(seedPhraseString).toEntropy().toHex() - val encryptedEntropyHex = App.appCore.getCurrentAuthenticationManager() - .encryptInBackground(password, entropyHex) - ?: return false - return setStringWithResult(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX, encryptedEntropyHex) - } - - /** - * Saves the seed in HEX. This method is **only** to be used in case of - * recovering a wallet from a seed (Wallet private key). - * In other situation, like creation of a new wallet or recovering it from a seed phrase, - * [tryToSetEncryptedSeedPhrase] must be used instead. - * - * @see getSeedHex - */ - suspend fun tryToSetEncryptedSeedHex(seedHex: String, password: String): Boolean { - val encryptedSeedHex = App.appCore.getCurrentAuthenticationManager() - .encryptInBackground(password, seedHex) - ?: return false - return setStringWithResult(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED, encryptedSeedHex) - } - - suspend fun getSeedHex(password: String): String { - val authenticationManager = App.appCore.getOriginalAuthenticationManager() - - // Try the encrypted entropy hex. - getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX) - ?.let { authenticationManager.decryptInBackground(password, it) } - ?.let { Mnemonics.MnemonicCode(it.hexToBytes()).toSeed().toHex() } - ?.let { return it } - - // Try the legacy encrypted seed hex. - getString(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED) - ?.let { authenticationManager.decryptInBackground(password, it) } - ?.let { return it } - - error("Failed to get the seed") - } - - suspend fun getSeedHex(decryptKey: SecretKey): String { - val authenticationManager = App.appCore.getOriginalAuthenticationManager() - - // Try the encrypted entropy hex. - getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX) - ?.let { authenticationManager.decryptInBackground(decryptKey, it) } - ?.let { Mnemonics.MnemonicCode(it.hexToBytes()).toSeed().toHex() } - ?.let { return it } - - // Try the legacy encrypted seed hex. - getString(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED) - ?.let { authenticationManager.decryptInBackground(decryptKey, it) } - ?.let { return it } - - error("Failed to get the seed") - } - - /** - * @see hasEncryptedSeedPhrase - */ - suspend fun getSeedPhrase(password: String): String = - getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX) - ?.let { - App.appCore.getOriginalAuthenticationManager().decryptInBackground(password, it) - } - ?.let { - Mnemonics.MnemonicCode(it.hexToBytes()).words.joinToString( - separator = " ", - transform = CharArray::concatToString - ) - } - ?: error("Failed to get the seed phrase") - - /** - * @see hasEncryptedSeedPhrase - */ - suspend fun getSeedPhrase(decryptKey: SecretKey): String = - getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX) - ?.let { - App.appCore.getOriginalAuthenticationManager().decryptInBackground(decryptKey, it) - } - ?.let { - Mnemonics.MnemonicCode(it.hexToBytes()).words.joinToString( - separator = " ", - transform = CharArray::concatToString - ) - } - ?: error("Failed to get the seed phrase") - - fun updateEncryptedSeedHex(encryptedSeedHex: String): Boolean { - return setStringWithResult(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED, encryptedSeedHex) - } - - fun updateEncryptedSeedEntropyHex(encryptedSeedEntropyHex: String): Boolean { - return setStringWithResult(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX, encryptedSeedEntropyHex) - } - - fun hasEncryptedSeed(): Boolean = - getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX) != null - || getString(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED) != null - - /** - * The seed phrase can be restored only if it has been saved as an entropy. - * - * @see getSeedPhrase - */ - fun hasEncryptedSeedPhrase(): Boolean = - getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX) != null -} diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/FilterPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/FilterPreferences.kt deleted file mode 100644 index ba561293..00000000 --- a/app/src/main/java/com/concordium/wallet/data/preferences/FilterPreferences.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.concordium.wallet.data.preferences - -import android.content.Context - -class FilterPreferences(val context: Context) : - Preferences(context, SharedPreferencesKeys.PREF_FILE_FILTER.key, Context.MODE_PRIVATE) { - - companion object { - val PREFKEY_FILTER_SHOW_REWARDS = "PREFKEY_FILTER_SHOW_REWARDS" - val PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS = "PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS" - } - - fun setHasShowRewards(id: Int, value: Boolean) { - setBoolean(PREFKEY_FILTER_SHOW_REWARDS + id, value) - } - - fun getHasShowRewards(id: Int): Boolean { - return getBoolean(PREFKEY_FILTER_SHOW_REWARDS + id, true) - } - - fun setHasShowFinalizationRewards(id: Int, value: Boolean) { - setBoolean(PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS + id, value) - } - - fun getHasShowFinalizationRewards(id: Int): Boolean { - return getBoolean(PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS + id, true) - } -} diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/Preferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/Preferences.kt index 71bcd655..85b46a0a 100644 --- a/app/src/main/java/com/concordium/wallet/data/preferences/Preferences.kt +++ b/app/src/main/java/com/concordium/wallet/data/preferences/Preferences.kt @@ -2,19 +2,20 @@ package com.concordium.wallet.data.preferences import android.content.Context import android.content.SharedPreferences +import com.concordium.wallet.App +import com.google.gson.Gson import kotlin.reflect.KProperty -open class Preferences { +open class Preferences(context: Context, preferenceName: String) { - protected lateinit var sharedPreferences: SharedPreferences + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE) - protected val editor: android.content.SharedPreferences.Editor + private val editor: SharedPreferences.Editor get() = sharedPreferences.edit() private val changeListeners = HashMap() - constructor() {} - interface Listener { fun onChange() } @@ -35,10 +36,6 @@ open class Preferences { ) = setBoolean(key, arg) } - constructor(context: Context, preferenceName: String, preferenceMode: Int) { - sharedPreferences = context.getSharedPreferences(preferenceName, preferenceMode) - } - fun triggerChangeEvent(key: String) { for ((listener, value) in changeListeners) { if (value == key) { @@ -53,11 +50,11 @@ open class Preferences { editor.commit() } - public fun addListener(key: String, listener: Listener) { + fun addListener(key: String, listener: Listener) { changeListeners.put(listener, key) } - public fun removeListener(listener: Listener) { + fun removeListener(listener: Listener) { changeListeners.remove(listener) } @@ -139,4 +136,17 @@ open class Preferences { protected fun getLong(key: String): Long { return sharedPreferences.getLong(key, 0) } + + protected fun setJsonSerialized( + key: String, + value: T?, + gson: Gson = App.appCore.gson, + ) = setString(key, value?.let(gson::toJson)) + + protected inline fun getJsonSerialized( + key: String, + gson: Gson = App.appCore.gson, + ): T? = runCatching { + getString(key)?.let { gson.fromJson(it, T::class.java) } + }.getOrNull() } diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferenceFiles.kt b/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferenceFiles.kt new file mode 100644 index 00000000..bf5530b8 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferenceFiles.kt @@ -0,0 +1,14 @@ +package com.concordium.wallet.data.preferences + +enum class SharedPreferenceFiles(val key: String) { + APP_SETUP("PREF_FILE_APP_SETUP"), + APP_TRACKING("PREF_TRACKING"), + + WALLET_SETUP("PREF_FILE_WALLET_SETUP"), + WALLET_FILTER("PREF_FILE_FILTER"), + WALLET_PROVIDER("PREF_FILE_PROVIDER"), + WALLET_ID_CREATION_DATA("KEY_IDENTITY_CREATION_DATA"), + WALLET_NOTIFICATIONS("PREF_NOTIFICATION"), + WALLET_SEND_FUNDS("PREF_SEND_FUNDS"), + ; +} diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferencesKeys.kt b/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferencesKeys.kt deleted file mode 100644 index e8a6ae52..00000000 --- a/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferencesKeys.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.concordium.wallet.data.preferences - -enum class SharedPreferencesKeys(val key: String) { - PREF_FILE_AUTH("PREF_FILE_AUTH"), - PREF_FILE_FILTER("PREF_FILE_FILTER"), - PREF_FILE_PROVIDER("PREF_FILE_PROVIDER"), - KEY_IDENTITY_CREATION_DATA("KEY_IDENTITY_CREATION_DATA"), - PREF_NOTIFICATION("PREF_NOTIFICATION"), - PREF_TRACKING("PREF_TRACKING"), - ; -} diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/WalletFilterPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletFilterPreferences.kt new file mode 100644 index 00000000..c1d44ed5 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletFilterPreferences.kt @@ -0,0 +1,38 @@ +package com.concordium.wallet.data.preferences + +import android.content.Context + +class WalletFilterPreferences +@Deprecated( + message = "Do not construct instances on your own", + replaceWith = ReplaceWith( + expression = "App.appCore.session.walletStorage.filterPreferences", + imports = arrayOf("com.concordium.wallet.App"), + ) +) +constructor( + val context: Context, + fileNameSuffix: String = "", +) : Preferences(context, SharedPreferenceFiles.WALLET_FILTER.key + fileNameSuffix) { + + private companion object { + const val PREFKEY_FILTER_SHOW_REWARDS = "PREFKEY_FILTER_SHOW_REWARDS" + const val PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS = "PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS" + } + + fun setHasShowRewards(id: Int, value: Boolean) { + setBoolean(PREFKEY_FILTER_SHOW_REWARDS + id, value) + } + + fun getHasShowRewards(id: Int): Boolean { + return getBoolean(PREFKEY_FILTER_SHOW_REWARDS + id, true) + } + + fun setHasShowFinalizationRewards(id: Int, value: Boolean) { + setBoolean(PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS + id, value) + } + + fun getHasShowFinalizationRewards(id: Int): Boolean { + return getBoolean(PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS + id, true) + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/WalletIdentityCreationDataPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletIdentityCreationDataPreferences.kt new file mode 100644 index 00000000..be1e5f8b --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletIdentityCreationDataPreferences.kt @@ -0,0 +1,37 @@ +package com.concordium.wallet.data.preferences + +import android.content.Context +import com.concordium.wallet.data.model.IdentityCreationData + +class WalletIdentityCreationDataPreferences +@Deprecated( + message = "Do not construct instances on your own", + replaceWith = ReplaceWith( + expression = "App.appCore.session.walletStorage.identityCreationDataPreferences", + imports = arrayOf("com.concordium.wallet.App"), + ) +) +constructor( + context: Context, + fileNameSuffix: String = "", +) : Preferences(context, SharedPreferenceFiles.WALLET_ID_CREATION_DATA.key + fileNameSuffix) { + + fun getIdentityCreationData(): IdentityCreationData? = + getJsonSerialized(PREFKEY_IDENTITY_CREATION_DATA) + + fun setIdentityCreationData(data: IdentityCreationData?) = + setJsonSerialized(PREFKEY_IDENTITY_CREATION_DATA, data) + + fun getShowForFirstIdentityFromCallback(): Boolean { + return getBoolean(PREFKEY_SHOW_FOR_FIRST_IDENTITY, false) + } + + fun setShowForFirstIdentityFromCallback(isFirst: Boolean) { + setBoolean(PREFKEY_SHOW_FOR_FIRST_IDENTITY, isFirst) + } + + private companion object { + const val PREFKEY_IDENTITY_CREATION_DATA = "KEY_IDENTITY_CREATION_DATA" + const val PREFKEY_SHOW_FOR_FIRST_IDENTITY = "SHOW_FOR_FIRST_IDENTITY" + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/NotificationsPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletNotificationsPreferences.kt similarity index 70% rename from app/src/main/java/com/concordium/wallet/data/preferences/NotificationsPreferences.kt rename to app/src/main/java/com/concordium/wallet/data/preferences/WalletNotificationsPreferences.kt index e3fa0be7..688a8a32 100644 --- a/app/src/main/java/com/concordium/wallet/data/preferences/NotificationsPreferences.kt +++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletNotificationsPreferences.kt @@ -2,8 +2,18 @@ package com.concordium.wallet.data.preferences import android.content.Context -class NotificationsPreferences(context: Context) : - Preferences(context, SharedPreferencesKeys.PREF_NOTIFICATION.key, Context.MODE_PRIVATE) { +class WalletNotificationsPreferences +@Deprecated( + message = "Do not construct instances on your own", + replaceWith = ReplaceWith( + expression = "App.appCore.session.walletStorage.notificationsPreferences", + imports = arrayOf("com.concordium.wallet.App"), + ) +) +constructor( + context: Context, + fileNameSuffix: String = "", +) : Preferences(context, SharedPreferenceFiles.WALLET_NOTIFICATIONS.key + fileNameSuffix) { var hasEverShownPermissionDialog: Boolean by BooleanPreference(PREFKEY_HAS_EVER_SHOWN_PERMISSION_DIALOG, false) diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/ProviderPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletProviderPreferences.kt similarity index 73% rename from app/src/main/java/com/concordium/wallet/data/preferences/ProviderPreferences.kt rename to app/src/main/java/com/concordium/wallet/data/preferences/WalletProviderPreferences.kt index d2524541..763d5457 100644 --- a/app/src/main/java/com/concordium/wallet/data/preferences/ProviderPreferences.kt +++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletProviderPreferences.kt @@ -4,7 +4,18 @@ import android.content.Context import com.concordium.wallet.ui.tokens.provider.ProviderMeta import com.google.gson.Gson -class ProviderPreferences(val context: Context) : Preferences(context, SharedPreferencesKeys.PREF_FILE_PROVIDER.key, Context.MODE_PRIVATE) { +class WalletProviderPreferences +@Deprecated( + message = "Do not construct instances on your own", + replaceWith = ReplaceWith( + expression = "App.appCore.session.walletStorage.providerPreferences", + imports = arrayOf("com.concordium.wallet.App"), + ) +) +constructor( + val context: Context, + fileNameSuffix: String = "", +) : Preferences(context, SharedPreferenceFiles.WALLET_PROVIDER.key + fileNameSuffix) { private val gson by lazy { Gson() diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/WalletSendFundsPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletSendFundsPreferences.kt new file mode 100644 index 00000000..d799bb66 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletSendFundsPreferences.kt @@ -0,0 +1,29 @@ +package com.concordium.wallet.data.preferences + +import android.content.Context + +class WalletSendFundsPreferences +@Deprecated( + message = "Do not construct instances on your own", + replaceWith = ReplaceWith( + expression = "App.appCore.session.walletStorage.sendFundsPreferences", + imports = arrayOf("com.concordium.wallet.App"), + ) +) +constructor( + context: Context, + fileNameSuffix: String = "", +) : Preferences(context, SharedPreferenceFiles.WALLET_SEND_FUNDS.key + fileNameSuffix) { + + fun shouldShowMemoWarning(): Boolean { + return getBoolean(KEY_SHOW_MEMO_WARNING, true) + } + + fun disableShowMemoWarning() { + setBoolean(KEY_SHOW_MEMO_WARNING, false) + } + + private companion object { + const val KEY_SHOW_MEMO_WARNING = "KEY_SHOW_MEMO_WARNING_V2" + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/WalletSetupPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletSetupPreferences.kt new file mode 100644 index 00000000..36483adc --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletSetupPreferences.kt @@ -0,0 +1,156 @@ +package com.concordium.wallet.data.preferences + +import android.content.Context +import cash.z.ecc.android.bip39.Mnemonics +import cash.z.ecc.android.bip39.toSeed +import com.concordium.wallet.App +import com.concordium.wallet.data.model.EncryptedData +import com.concordium.wallet.util.toHex +import com.google.gson.Gson +import okio.ByteString.Companion.decodeHex + +class WalletSetupPreferences +@Deprecated( + message = "Do not construct instances on your own", + replaceWith = ReplaceWith( + expression = "App.appCore.session.walletStorage.setupPreferences", + imports = arrayOf("com.concordium.wallet.App"), + ) +) +constructor( + context: Context, + fileNameSuffix: String = "", + private val gson: Gson = App.appCore.gson, +) : Preferences(context, SharedPreferenceFiles.WALLET_SETUP.key + fileNameSuffix) { + + fun areAccountsBackedUp(): Boolean { + return getBoolean(PREFKEY_ACCOUNTS_BACKED_UP, true) + } + + fun setAccountsBackedUp(value: Boolean) { + return setBoolean(PREFKEY_ACCOUNTS_BACKED_UP, value) + } + + fun addAccountsBackedUpListener(listener: Listener) { + addListener(PREFKEY_ACCOUNTS_BACKED_UP, listener) + } + + /** + * Saves the seed phrase as its entropy, so it is possible to later get + * both the seed hex and the seed phrase. + * + * @see getSeedHex + * @see getSeedPhrase + */ + suspend fun tryToSetEncryptedSeedPhrase(seedPhraseString: String, password: String): Boolean { + val entropy = Mnemonics.MnemonicCode(seedPhraseString).toEntropy() + val encryptedEntropy = App.appCore.auth.encrypt( + password = password, + // The entropy is encoded to hex to keep backward compatibility + // with the data encrypted before the Two wallets feature. + data = entropy.toHex().toByteArray(), + ) ?: return false + return tryToSetEncryptedSeedPhrase(encryptedEntropy) + } + + fun tryToSetEncryptedSeedPhrase(encryptedSeedEntropy: EncryptedData): Boolean = + setStringWithResult( + PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON, + gson.toJson(encryptedSeedEntropy) + ) + + /** + * Saves the seed in HEX. This method is **only** to be used in case of + * recovering a wallet from a seed (Wallet private key). + * In other situation, like creation of a new wallet or recovering it from a seed phrase, + * [tryToSetEncryptedSeedPhrase] must be used instead. + * + * @see getSeedHex + */ + suspend fun tryToSetEncryptedSeedHex(seedHex: String, password: String): Boolean { + val encryptedSeed = App.appCore.auth.encrypt( + password = password, + // The seed is not decoded from hex to keep backward compatibility + // with the data encrypted before the Two wallets feature. + data = seedHex.toByteArray(), + ) ?: return false + return tryToSetEncryptedSeedHex(encryptedSeed) + } + + fun tryToSetEncryptedSeedHex(encryptedSeedHex: EncryptedData): Boolean = + setStringWithResult( + PREFKEY_ENCRYPTED_SEED_HEX_JSON, + gson.toJson(encryptedSeedHex) + ) + + suspend fun getSeedHex(password: String): String { + val authenticationManager = App.appCore.auth + + // Try the encrypted entropy. + getJsonSerialized(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON, gson) + ?.let { authenticationManager.decrypt(password, it) } + ?.let { String(it).decodeHex().toByteArray() } + ?.let { Mnemonics.MnemonicCode(it).toSeed().toHex() } + ?.also { return it } + + // Try the encrypted seed. + getJsonSerialized(PREFKEY_ENCRYPTED_SEED_HEX_JSON, gson) + ?.let { authenticationManager.decrypt(password, it) } + ?.let(::String) + ?.also { return it } + + error("Failed to get the seed") + } + + /** + * @see hasEncryptedSeedPhrase + */ + suspend fun getSeedPhrase(password: String): String = + getJsonSerialized(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON, gson) + ?.let { App.appCore.auth.decrypt(password, it) } + ?.let { String(it).decodeHex().toByteArray() } + ?.let { + Mnemonics.MnemonicCode(it).words.joinToString( + separator = " ", + transform = CharArray::concatToString + ) + } + ?: error("Failed to get the seed phrase") + + fun hasEncryptedSeed(): Boolean = + getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON) != null + || getString(PREFKEY_ENCRYPTED_SEED_HEX_JSON) != null + + /** + * The seed phrase can be restored only if it has been saved as an entropy. + * + * @see getSeedPhrase + */ + fun hasEncryptedSeedPhrase(): Boolean = + getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON) != null + + fun setHasCompletedOnboarding(value: Boolean) { + setBoolean(PREFKEY_HAS_COMPLETED_ONBOARDING, value) + } + + fun getHasCompletedOnboarding(): Boolean { + return getBoolean(PREFKEY_HAS_COMPLETED_ONBOARDING, false) + } + + fun setHasShownInitialAnimation(value: Boolean) { + setBoolean(PREFKEY_HAS_SHOWN_INITIAL_ANIMATION, value) + } + + fun getHasShownInitialAnimation(): Boolean { + return getBoolean(PREFKEY_HAS_SHOWN_INITIAL_ANIMATION, false) + } + + private companion object { + const val PREFKEY_ACCOUNTS_BACKED_UP = "PREFKEY_ACCOUNTS_BACKED_UP" + const val PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON = + "PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON" + const val PREFKEY_ENCRYPTED_SEED_HEX_JSON = "PREFKEY_ENCRYPTED_SEED_HEX_JSON" + const val PREFKEY_HAS_COMPLETED_ONBOARDING = "PREFKEY_HAS_COMPLETED_ONBOARDING" + const val PREFKEY_HAS_SHOWN_INITIAL_ANIMATION = "PREFKEY_HAS_SHOWN_INITIAL_ANIMATION" + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/room/Account.kt b/app/src/main/java/com/concordium/wallet/data/room/Account.kt index 08411ad2..2d602458 100644 --- a/app/src/main/java/com/concordium/wallet/data/room/Account.kt +++ b/app/src/main/java/com/concordium/wallet/data/room/Account.kt @@ -12,6 +12,7 @@ import com.concordium.wallet.data.model.AccountDelegation import com.concordium.wallet.data.model.AccountEncryptedAmount import com.concordium.wallet.data.model.AccountReleaseSchedule import com.concordium.wallet.data.model.CredentialWrapper +import com.concordium.wallet.data.model.EncryptedData import com.concordium.wallet.data.model.IdentityAttribute import com.concordium.wallet.data.model.ShieldedAccountEncryptionStatus import com.concordium.wallet.data.model.TransactionStatus @@ -42,7 +43,7 @@ data class Account( var transactionStatus: TransactionStatus, @ColumnInfo(name = "encrypted_account_data") - var encryptedAccountData: String, + var encryptedAccountData: EncryptedData?, @ColumnInfo("credential") var credential: CredentialWrapper?, diff --git a/app/src/main/java/com/concordium/wallet/data/room/Identity.kt b/app/src/main/java/com/concordium/wallet/data/room/Identity.kt index d1239a33..4620e7f6 100644 --- a/app/src/main/java/com/concordium/wallet/data/room/Identity.kt +++ b/app/src/main/java/com/concordium/wallet/data/room/Identity.kt @@ -1,6 +1,11 @@ package com.concordium.wallet.data.room -import androidx.room.* +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.concordium.wallet.data.model.EncryptedData import com.concordium.wallet.data.model.IdentityObject import com.concordium.wallet.data.model.IdentityProvider import com.concordium.wallet.data.room.typeconverter.IdentityTypeConverters @@ -26,7 +31,7 @@ data class Identity( @ColumnInfo(name = "identity_object") var identityObject: IdentityObject?, @ColumnInfo(name = "private_id_object_data_encrypted") - var privateIdObjectDataEncrypted: String, // Used for V0 key creation. + var privateIdObjectDataEncrypted: EncryptedData?, // Used for V0 key creation. @ColumnInfo(name = "identity_provider_id") var identityProviderId: Int, @ColumnInfo(name = "identity_index") diff --git a/app/src/main/java/com/concordium/wallet/data/room/WalletDatabase.kt b/app/src/main/java/com/concordium/wallet/data/room/WalletDatabase.kt index a3a7a877..c64d4fce 100644 --- a/app/src/main/java/com/concordium/wallet/data/room/WalletDatabase.kt +++ b/app/src/main/java/com/concordium/wallet/data/room/WalletDatabase.kt @@ -1,17 +1,20 @@ package com.concordium.wallet.data.room import android.content.Context +import androidx.collection.LruCache import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import com.concordium.wallet.core.AppCore import com.concordium.wallet.data.room.WalletDatabase.Companion.VERSION_NUMBER import com.concordium.wallet.data.room.migrations.MIGRATION_3_4 import com.concordium.wallet.data.room.migrations.MIGRATION_4_5 import com.concordium.wallet.data.room.migrations.MIGRATION_5_6 import com.concordium.wallet.data.room.migrations.MIGRATION_7_8 import com.concordium.wallet.data.room.migrations.MIGRATION_8_9 +import com.concordium.wallet.data.room.migrations.MIGRATION_9_10 import com.concordium.wallet.data.room.typeconverter.GlobalTypeConverters @Database( @@ -30,7 +33,7 @@ import com.concordium.wallet.data.room.typeconverter.GlobalTypeConverters ], ) @TypeConverters(GlobalTypeConverters::class) -public abstract class WalletDatabase : RoomDatabase() { +abstract class WalletDatabase : RoomDatabase() { abstract fun identityDao(): IdentityDao abstract fun accountDao(): AccountDao @@ -40,23 +43,36 @@ public abstract class WalletDatabase : RoomDatabase() { abstract fun contractTokenDao(): ContractTokenDao companion object { + const val VERSION_NUMBER = 10 + private val instances = object : LruCache(2) { + override fun entryRemoved( + evicted: Boolean, + key: String, + oldValue: WalletDatabase, + newValue: WalletDatabase?, + ) { + oldValue.close() + } + } - const val VERSION_NUMBER = 9 - - // Singleton prevents multiple instances of database opening at the same time. - @Volatile - private var INSTANCE: WalletDatabase? = null + @Deprecated( + message = "Do not construct instances on your own", + replaceWith = ReplaceWith( + expression = "App.appCore.session.walletStorage.database", + imports = arrayOf("com.concordium.wallet.App"), + ) + ) + fun getDatabase( + context: Context, + fileNameSuffix: String = "", + ): WalletDatabase = synchronized(this) { + val name = "wallet_database$fileNameSuffix" - fun getDatabase(context: Context): WalletDatabase { - val tempInstance = INSTANCE - if (tempInstance != null) { - return tempInstance - } - synchronized(this) { - val instance = Room.databaseBuilder( + instances[name] + ?: Room.databaseBuilder( context.applicationContext, WalletDatabase::class.java, - "wallet_database" + "wallet_database$fileNameSuffix" ) .fallbackToDestructiveMigration() // See auto migrations in the @Database declaration. @@ -66,11 +82,13 @@ public abstract class WalletDatabase : RoomDatabase() { MIGRATION_5_6, MIGRATION_7_8, MIGRATION_8_9, + MIGRATION_9_10( + context = context, + gson = AppCore.getGson(), + ), ) .build() - INSTANCE = instance - return instance - } + .also { instances.put(name, it) } } } } diff --git a/app/src/main/java/com/concordium/wallet/data/room/app/AppDatabase.kt b/app/src/main/java/com/concordium/wallet/data/room/app/AppDatabase.kt new file mode 100644 index 00000000..6882f658 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/room/app/AppDatabase.kt @@ -0,0 +1,34 @@ +package com.concordium.wallet.data.room.app + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database( + entities = [ + AppWalletEntity::class, + ], + version = 1, + exportSchema = true, +) +abstract class AppDatabase : RoomDatabase() { + + abstract fun appWalletDao(): AppWalletDao + + companion object { + private const val FILE_NAME = "app_database" + private var instance: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase = synchronized(this) { + instance + ?: Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + FILE_NAME, + ) + .build() + .also(::instance::set) + } + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletDao.kt b/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletDao.kt new file mode 100644 index 00000000..6724dd54 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletDao.kt @@ -0,0 +1,49 @@ +package com.concordium.wallet.data.room.app + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow + +@Dao +abstract class AppWalletDao { + @Query("SELECT * FROM wallets ORDER BY created_at ASC") + abstract fun getAll(): Flow> + + @Query("SELECT COUNT(*) FROM wallets") + abstract suspend fun getCount(): Int + + @Query("SELECT * FROM WALLETS WHERE is_active=1") + abstract suspend fun getActive(): AppWalletEntity + + @Query("UPDATE wallets SET is_active = CASE WHEN id=:walletId THEN 1 ELSE 0 END") + abstract suspend fun activate(walletId: String) + + @Insert + protected abstract suspend fun insert(wallet: AppWalletEntity): Long + + @Transaction + open suspend fun insertAndActivate(wallet: AppWalletEntity) { + insert(wallet) + activate(wallet.id) + } + + @Query("UPDATE wallets SET type=:newType WHERE id=:walletId") + abstract suspend fun switchType( + walletId: String, + newType: String, + ) + + @Query("DELETE FROM wallets WHERE id=:walletId") + protected abstract suspend fun delete(walletId: String) + + @Transaction + open suspend fun deleteAndActivateAnother( + walletToDeleteId: String, + walletToActivateId: String, + ) { + delete(walletToDeleteId) + activate(walletToActivateId) + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletEntity.kt b/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletEntity.kt new file mode 100644 index 00000000..29db78b8 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletEntity.kt @@ -0,0 +1,36 @@ +package com.concordium.wallet.data.room.app + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import com.concordium.wallet.core.multiwallet.AppWallet + +@Entity( + tableName = "wallets", + indices = [ + Index("created_at"), + Index("is_active"), + ] +) +class AppWalletEntity( + @PrimaryKey + @ColumnInfo(name = "id") + val id: String, + @ColumnInfo(name = "type") + val type: String, + @ColumnInfo(name = "created_at") + val createdAt: Long, + @ColumnInfo(name = "is_active") + val isActive: Boolean, +) { + constructor( + wallet: AppWallet, + isActive: Boolean = false, + ) : this( + id = wallet.id, + type = wallet.type.name, + createdAt = wallet.createdAt.time, + isActive = isActive, + ) +} diff --git a/app/src/main/java/com/concordium/wallet/data/room/migrations/MIGRATION_9_10.kt b/app/src/main/java/com/concordium/wallet/data/room/migrations/MIGRATION_9_10.kt new file mode 100644 index 00000000..5fbf7276 --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/data/room/migrations/MIGRATION_9_10.kt @@ -0,0 +1,94 @@ +package com.concordium.wallet.data.room.migrations + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.concordium.wallet.core.migration.TwoWalletsMigration +import com.concordium.wallet.data.model.EncryptedData +import com.concordium.wallet.data.room.typeconverter.GlobalTypeConverters +import com.google.gson.Gson + +fun MIGRATION_9_10( + context: Context, + gson: Gson, +) = object : Migration(9, 10) { + private val globalTypeConverters = GlobalTypeConverters() + private val twoWalletsMigration = TwoWalletsMigration( + context = context, + gson = gson, + ) + + override fun migrate(database: SupportSQLiteDatabase) = with(database) { + execSQL("CREATE TABLE IF NOT EXISTS `_new_identity_table` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `status` TEXT NOT NULL, `detail` TEXT, `code_uri` TEXT NOT NULL, `next_account_number` INTEGER NOT NULL, `identity_provider` TEXT NOT NULL, `identity_object` TEXT, `private_id_object_data_encrypted` TEXT, `identity_provider_id` INTEGER NOT NULL, `identity_index` INTEGER NOT NULL)") + execSQL("INSERT INTO `_new_identity_table` (`id`,`name`,`status`,`detail`,`code_uri`,`next_account_number`,`identity_provider`,`identity_object`,`private_id_object_data_encrypted`,`identity_provider_id`,`identity_index`) SELECT `id`,`name`,`status`,`detail`,`code_uri`,`next_account_number`,`identity_provider`,`identity_object`,`private_id_object_data_encrypted`,`identity_provider_id`,`identity_index` FROM `identity_table`") + execSQL("DROP TABLE `identity_table`") + execSQL("ALTER TABLE `_new_identity_table` RENAME TO `identity_table`") + execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_identity_table_identity_provider_id_identity_index` ON `identity_table` (`identity_provider_id`, `identity_index`)") + + // Migrate identity encrypted data: + // wrap the existing data into EncryptedData, or overwrite with null if missing. + database.query("SELECT `id`, `private_id_object_data_encrypted` FROM `identity_table`") + .use { identitiesCursor -> + while (identitiesCursor.moveToNext()) { + val oldPrivateIdObjectDataEncrypted = identitiesCursor.getString(1) + ?.takeIf(String::isNotEmpty) + val identityId = identitiesCursor.getInt(0) + val migratedPrivateIdObjectDataEncrypted: EncryptedData? = + oldPrivateIdObjectDataEncrypted + ?.let(twoWalletsMigration::migrateOldEncryptedData) + + update( + "identity_table", + SQLiteDatabase.CONFLICT_FAIL, + ContentValues(1).apply { + put( + "private_id_object_data_encrypted", + globalTypeConverters.encryptedDataToJson( + migratedPrivateIdObjectDataEncrypted + ) + ) + }, + "`id`=?", + arrayOf(identityId) + ) + } + } + + execSQL("CREATE TABLE IF NOT EXISTS `_new_account_table` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `identity_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `submission_id` TEXT NOT NULL, `transaction_status` INTEGER NOT NULL, `encrypted_account_data` TEXT, `credential` TEXT, `cred_number` INTEGER NOT NULL, `revealed_attributes` TEXT NOT NULL, `finalized_balance` TEXT NOT NULL, `balance_at_disposal` TEXT NOT NULL, `total_shielded_balance` TEXT NOT NULL, `finalized_encrypted_balance` TEXT, `current_balance_status` INTEGER NOT NULL, `read_only` INTEGER NOT NULL, `finalized_account_release_schedule` TEXT, `cooldowns` TEXT NOT NULL, `account_delegation` TEXT, `account_baker` TEXT, `accountIndex` INTEGER)") + execSQL("INSERT INTO `_new_account_table` (`id`,`identity_id`,`name`,`address`,`submission_id`,`transaction_status`,`encrypted_account_data`,`credential`,`cred_number`,`revealed_attributes`,`finalized_balance`,`balance_at_disposal`,`total_shielded_balance`,`finalized_encrypted_balance`,`current_balance_status`,`read_only`,`finalized_account_release_schedule`,`cooldowns`,`account_delegation`,`account_baker`,`accountIndex`) SELECT `id`,`identity_id`,`name`,`address`,`submission_id`,`transaction_status`,`encrypted_account_data`,`credential`,`cred_number`,`revealed_attributes`,`finalized_balance`,`balance_at_disposal`,`total_shielded_balance`,`finalized_encrypted_balance`,`current_balance_status`,`read_only`,`finalized_account_release_schedule`,`cooldowns`,`account_delegation`,`account_baker`,`accountIndex` FROM `account_table`") + execSQL("DROP TABLE `account_table`") + execSQL("ALTER TABLE `_new_account_table` RENAME TO `account_table`") + execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_account_table_address` ON `account_table` (`address`)") + + // Migrate account encrypted data: + // wrap the existing data into EncryptedData, or overwrite with null if missing. + database.query("SELECT `id`, `encrypted_account_data` FROM `account_table`") + .use { accountsCursor -> + while (accountsCursor.moveToNext()) { + val oldEncryptedAccountData = accountsCursor.getString(1) + ?.takeIf(String::isNotEmpty) + val accountId = accountsCursor.getInt(0) + val migratedEncryptedAccountData: EncryptedData? = + oldEncryptedAccountData + ?.let(twoWalletsMigration::migrateOldEncryptedData) + + update( + "account_table", + SQLiteDatabase.CONFLICT_FAIL, + ContentValues(1).apply { + put( + "encrypted_account_data", + globalTypeConverters.encryptedDataToJson( + migratedEncryptedAccountData + ) + ) + }, + "`id`=?", + arrayOf(accountId) + ) + } + } + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/room/typeconverter/GlobalTypeConverters.kt b/app/src/main/java/com/concordium/wallet/data/room/typeconverter/GlobalTypeConverters.kt index cc079715..e8d0819a 100644 --- a/app/src/main/java/com/concordium/wallet/data/room/typeconverter/GlobalTypeConverters.kt +++ b/app/src/main/java/com/concordium/wallet/data/room/typeconverter/GlobalTypeConverters.kt @@ -1,14 +1,16 @@ package com.concordium.wallet.data.room.typeconverter import androidx.room.TypeConverter +import com.concordium.wallet.core.AppCore +import com.concordium.wallet.data.model.EncryptedData import com.concordium.wallet.data.model.ShieldedAccountEncryptionStatus -import com.concordium.wallet.data.model.TransactionOriginType import com.concordium.wallet.data.model.TransactionOutcome import com.concordium.wallet.data.model.TransactionStatus import com.concordium.wallet.util.toBigInteger import java.math.BigInteger class GlobalTypeConverters { + private val gson = AppCore.getGson() @TypeConverter fun intToTransactionStatus(value: Int): TransactionStatus { @@ -41,22 +43,6 @@ class GlobalTypeConverters { return transactionOutcome.code } - @TypeConverter - fun intToTransactionOriginType(value: Int): TransactionOriginType { - return when (value) { - 0 -> TransactionOriginType.Self - 1 -> TransactionOriginType.Account - 2 -> TransactionOriginType.Reward - 3 -> TransactionOriginType.None - else -> TransactionOriginType.UNKNOWN - } - } - - @TypeConverter - fun transactionOriginTypeToInt(transactionOriginType: TransactionOriginType): Int { - return transactionOriginType.code - } - @TypeConverter fun intToShieldedAccountEncryptionStatus(value: Int): ShieldedAccountEncryptionStatus { return when (value) { @@ -81,4 +67,16 @@ class GlobalTypeConverters { fun stringToBigInteger(value: String?): BigInteger? { return value?.toBigInteger() } -} \ No newline at end of file + + @TypeConverter + fun encryptedDataToJson(value: EncryptedData?): String? { + return value?.let(gson::toJson) + } + + @TypeConverter + fun jsonToEncryptedData(value: String?): EncryptedData? { + return value?.takeIf(String::isNotEmpty)?.let { + gson.fromJson(it, EncryptedData::class.java) + } + } +} diff --git a/app/src/main/java/com/concordium/wallet/data/util/ExportEncryptionHelper.kt b/app/src/main/java/com/concordium/wallet/data/util/ExportEncryptionHelper.kt index 005e27f5..ba197141 100644 --- a/app/src/main/java/com/concordium/wallet/data/util/ExportEncryptionHelper.kt +++ b/app/src/main/java/com/concordium/wallet/data/util/ExportEncryptionHelper.kt @@ -1,10 +1,12 @@ package com.concordium.wallet.data.util +import android.security.keystore.KeyProperties import android.util.Base64 import com.concordium.wallet.core.security.EncryptionException import com.concordium.wallet.core.security.EncryptionHelper import com.concordium.wallet.data.export.EncryptedExportData import com.concordium.wallet.data.export.ExportEncryptionMetaData +import com.concordium.wallet.data.model.EncryptedData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -14,39 +16,71 @@ object ExportEncryptionHelper { * @exception EncryptionException */ @Throws(EncryptionException::class) - suspend fun encryptExportData(password: String, toBeEncrypted: String): EncryptedExportData = withContext(Dispatchers.Default) { - val iterations = 100000 - val (salt, iv) = EncryptionHelper.createEncryptionData() - val saltEncoded = Base64.encodeToString(salt, Base64.NO_WRAP) - val ivEncoded = Base64.encodeToString(iv, Base64.NO_WRAP) - val encryptionMetaData = - ExportEncryptionMetaData( - "AES-256", - "PBKDF2WithHmacSHA256", - iterations, - saltEncoded, - ivEncoded - ) - val key = EncryptionHelper.generateKey(password, salt, iterations) - // Encrypt (using NO_WRAP to avoid line break in the end) - val cipherText = EncryptionHelper.encrypt(key, iv, toBeEncrypted, Base64.NO_WRAP) - val encryptedExportData = EncryptedExportData(encryptionMetaData, cipherText) - return@withContext encryptedExportData + suspend fun encryptExportData( + password: String, + toBeEncrypted: String, + ): EncryptedExportData = withContext(Dispatchers.Default) { + // This method must remain backward-compatible with the Concordium file export, + // therefore it uses own encryption params and Base64 encoding. + + val salt = EncryptionHelper.generatePasswordKeySalt() + val key = EncryptionHelper.generatePasswordKey( + password = password.toCharArray(), + salt = salt, + sizeBits = ENCRYPTION_KEY_SIZE_BITS, + kdf = KDF, + kdfIterationCount = KDF_ITERATION_COUNT, + ) + val encryptedData = EncryptionHelper.encrypt( + key = key, + data = toBeEncrypted.toByteArray(), + cipherTransformation = CIPHER_TRANSFORMATION, + ) + + EncryptedExportData( + metadata = ExportEncryptionMetaData( + encryptionMethod = ENCRYPTION_METHOD, + keyDerivationMethod = KDF, + iterations = KDF_ITERATION_COUNT, + salt = Base64.encodeToString(salt, Base64.NO_WRAP), + initializationVector = Base64.encodeToString( + encryptedData.decodeIv(), + Base64.NO_WRAP + ), + ), + cipherText = Base64.encodeToString(encryptedData.decodeCiphertext(), Base64.NO_WRAP), + ) } @Throws(EncryptionException::class) - suspend fun decryptExportData(password: String, encryptedExportData: EncryptedExportData): String = withContext(Dispatchers.Default) { - val toBeEncryptedEncoded = encryptedExportData.cipherText - val toBeEncrypted = Base64.decode(toBeEncryptedEncoded, Base64.DEFAULT) - val iterations = encryptedExportData.metadata.iterations - val saltEncoded = encryptedExportData.metadata.salt - val ivEncoded = encryptedExportData.metadata.initializationVector - val salt = Base64.decode(saltEncoded, Base64.DEFAULT) - val iv = Base64.decode(ivEncoded, Base64.DEFAULT) - // Assume encryptionMethod: AES-256 and keyDerivationMethod: PBKDF2WithHmacSHA256 - // Because that is the only thing we support - val key = EncryptionHelper.generateKey(password, salt, iterations) - val decrypted = EncryptionHelper.decrypt(key, iv, toBeEncrypted) - return@withContext decrypted + suspend fun decryptExportData( + password: String, + encryptedExportData: EncryptedExportData, + ): String = withContext(Dispatchers.Default) { + val key = EncryptionHelper.generatePasswordKey( + password = password.toCharArray(), + salt = Base64.decode(encryptedExportData.metadata.salt, Base64.DEFAULT), + kdf = encryptedExportData.metadata.keyDerivationMethod, + kdfIterationCount = encryptedExportData.metadata.iterations, + sizeBits = ENCRYPTION_KEY_SIZE_BITS, + ) + val wrappedEncryptedExportData = EncryptedData( + ciphertext = Base64.decode(encryptedExportData.cipherText, Base64.DEFAULT), + iv = Base64.decode(encryptedExportData.metadata.initializationVector, Base64.DEFAULT), + transformation = CIPHER_TRANSFORMATION, + ) + + EncryptionHelper.decrypt( + key = key, + encryptedData = wrappedEncryptedExportData, + ).let(::String) } -} \ No newline at end of file + + private const val KDF_ITERATION_COUNT = 100000 + private const val KDF = "PBKDF2WithHmacSHA256" + private const val ENCRYPTION_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + private const val ENCRYPTION_KEY_SIZE_BITS = 256 + private const val ENCRYPTION_METHOD = "$ENCRYPTION_KEY_ALGORITHM-$ENCRYPTION_KEY_SIZE_BITS" + private const val CIPHER_TRANSFORMATION = + "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/PKCS7Padding" +} diff --git a/app/src/main/java/com/concordium/wallet/ui/MainActivity.kt b/app/src/main/java/com/concordium/wallet/ui/MainActivity.kt index 3fa80fd5..1a2a320c 100644 --- a/app/src/main/java/com/concordium/wallet/ui/MainActivity.kt +++ b/app/src/main/java/com/concordium/wallet/ui/MainActivity.kt @@ -7,11 +7,13 @@ import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.get import androidx.lifecycle.lifecycleScope import com.concordium.wallet.App import com.concordium.wallet.BuildConfig import com.concordium.wallet.R import com.concordium.wallet.databinding.ActivityMainBinding +import com.concordium.wallet.extension.collectWhenStarted import com.concordium.wallet.ui.account.accountsoverview.AccountsOverviewFragment import com.concordium.wallet.ui.auth.login.AuthLoginActivity import com.concordium.wallet.ui.base.BaseActivity @@ -21,12 +23,14 @@ import com.concordium.wallet.ui.common.delegates.IdentityStatusDelegate import com.concordium.wallet.ui.common.delegates.IdentityStatusDelegateImpl import com.concordium.wallet.ui.more.import.ImportActivity import com.concordium.wallet.ui.more.moreoverview.MoreOverviewFragment +import com.concordium.wallet.ui.multiwallet.WalletSwitchViewModel import com.concordium.wallet.ui.news.NewsOverviewFragment import com.concordium.wallet.ui.onboarding.OnboardingSharedViewModel import com.concordium.wallet.ui.tokens.provider.ProvidersOverviewFragment import com.concordium.wallet.ui.walletconnect.WalletConnectView import com.concordium.wallet.ui.walletconnect.WalletConnectViewModel import com.concordium.wallet.ui.welcome.WelcomeActivity +import com.concordium.wallet.ui.welcome.WelcomeRecoverWalletActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.launch @@ -35,6 +39,8 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over IdentityStatusDelegate by IdentityStatusDelegateImpl() { companion object { + const val EXTRA_IMPORT_FROM_FILE = "EXTRA_IMPORT_FROM_FILE" + const val EXTRA_IMPORT_FROM_SEED = "EXTRA_IMPORT_FROM_SEED" const val EXTRA_WALLET_CONNECT_URI = "wc_uri" } @@ -44,6 +50,7 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over private lateinit var viewModel: MainViewModel private lateinit var onboardingViewModel: OnboardingSharedViewModel private lateinit var walletConnectViewModel: WalletConnectViewModel + private lateinit var walletSwitchViewModel: WalletSwitchViewModel private var hasHandledPossibleImportFile = false //region Lifecycle @@ -73,6 +80,12 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over } handlePossibleWalletConnectUri(intent) + + if (intent.getBooleanExtra(EXTRA_IMPORT_FROM_FILE, false)) { + goToImportFromFile() + } else if (intent.getBooleanExtra(EXTRA_IMPORT_FROM_SEED, false)) { + goToImportFromSeed() + } } override fun onResume() { @@ -146,11 +159,12 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over // ************************************************************ private fun initializeViewModel() { - viewModel = ViewModelProvider( + val viewModelProvider = ViewModelProvider( this, ViewModelProvider.AndroidViewModelFactory.getInstance(application) - )[MainViewModel::class.java] + ) + viewModel = viewModelProvider.get() viewModel.titleLiveData.observe(this, this::setActionBarTitle) viewModel.stateLiveData.observe(this) { state -> checkNotNull(state) @@ -161,15 +175,15 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over replaceFragment(state) } - onboardingViewModel = ViewModelProvider( - this, - ViewModelProvider.AndroidViewModelFactory.getInstance(application) - )[OnboardingSharedViewModel::class.java] + walletConnectViewModel = viewModelProvider.get() + onboardingViewModel = viewModelProvider.get() - walletConnectViewModel = ViewModelProvider( - this, - ViewModelProvider.AndroidViewModelFactory.getInstance(application) - )[WalletConnectViewModel::class.java] + walletSwitchViewModel = viewModelProvider.get() + walletSwitchViewModel.switchesFlow.collectWhenStarted(this) { + // Force restart the activity with recreation of view models. + finishAffinity() + startActivity(Intent(this, MainActivity::class.java)) + } } private fun initializeViews() { @@ -187,6 +201,8 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over authDelegate = this, viewModel = walletConnectViewModel, ).init() + + binding.walletSwitchView.bind(walletSwitchViewModel) } //endregion @@ -276,5 +292,18 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over walletConnectViewModel.handleWcUri(walletConnectUri) } } + + private fun goToImportFromFile() { + val intent = Intent(this, ImportActivity::class.java) + startActivity(intent) + } + + private fun goToImportFromSeed() { + val intent = Intent(this, WelcomeRecoverWalletActivity::class.java) + intent.putExtras(WelcomeRecoverWalletActivity.getBundle( + showFileOptions = false, + )) + startActivity(intent) + } //endregion } diff --git a/app/src/main/java/com/concordium/wallet/ui/MainViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/MainViewModel.kt index 3d06bf9e..95dbb8ef 100644 --- a/app/src/main/java/com/concordium/wallet/ui/MainViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/MainViewModel.kt @@ -6,11 +6,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.concordium.wallet.App -import com.concordium.wallet.core.authentication.Session -import com.concordium.wallet.data.IdentityRepository import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.room.Identity -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.ui.common.identity.IdentityUpdater import com.concordium.wallet.util.Log import kotlinx.coroutines.launch @@ -25,9 +22,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { ; } - private val identityRepository: IdentityRepository private val identityUpdater = IdentityUpdater(application, viewModelScope) - private val session: Session = App.appCore.session var databaseVersionAllowed = true @@ -42,15 +37,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val canAcceptImportFiles: Boolean get() = App.appCore.session.isAccountsBackupPossible() - init { - val identityDao = WalletDatabase.getDatabase(application).identityDao() - identityRepository = IdentityRepository(identityDao) - } - fun initialize() { try { val dbVersion = - WalletDatabase.getDatabase(getApplication()).openHelper.readableDatabase.version.toString() + App.appCore.session.walletStorage.database.openHelper.readableDatabase.version.toString() } catch (e: Exception) { Log.e("Database init failed. Missing migrations?") e.printStackTrace() @@ -78,19 +68,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } fun shouldShowAuthentication(): Boolean { - session.isLoggedIn.value.let { return it == null || it == false } + App.appCore.session.isLoggedIn.value.let { return it == null || it == false } } fun shouldShowPasswordSetup(): Boolean { - return !session.hasSetupPassword + return !App.appCore.setup.isAuthSetupCompleted } fun shouldShowInitialSetup(): Boolean { - return session.hasSetupPassword && !session.hasCompletedInitialSetup + return !App.appCore.setup.isInitialSetupCompleted } fun hasCompletedOnboarding(): Boolean { - return session.hasCompleteOnboarding + return App.appCore.session.walletStorage.setupPreferences.getHasCompletedOnboarding() } fun startIdentityUpdate() { diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountDetailsViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountDetailsViewModel.kt index 6eaa2d1a..efe79403 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountDetailsViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountDetailsViewModel.kt @@ -9,7 +9,7 @@ import androidx.lifecycle.viewModelScope import com.concordium.wallet.App import com.concordium.wallet.BuildConfig import com.concordium.wallet.core.arch.Event -import com.concordium.wallet.core.authentication.Session +import com.concordium.wallet.core.Session import com.concordium.wallet.data.AccountRepository import com.concordium.wallet.data.IdentityRepository import com.concordium.wallet.data.RecipientRepository @@ -24,7 +24,6 @@ import com.concordium.wallet.data.model.TransactionType import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.room.Identity import com.concordium.wallet.data.room.Transfer -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.data.util.toTransaction import com.concordium.wallet.ui.account.accountdetails.transfers.AdapterItem import com.concordium.wallet.ui.account.accountdetails.transfers.HeaderItem @@ -40,25 +39,17 @@ import java.math.BigInteger import java.util.Date class AccountDetailsViewModel(application: Application) : AndroidViewModel(application) { - private fun MutableLiveData.forceRefresh() { - this.value = this.value - } - private val session: Session = App.appCore.session - var hasTransactionsToDecrypt: Boolean = false - lateinit var account: Account var hasPendingDelegationTransactions: Boolean = false var hasPendingBakingTransactions: Boolean = false private val proxyRepository = ProxyRepository() - private val accountRepository: AccountRepository - private val transferRepository: TransferRepository - private val identityRepository: IdentityRepository - private val recipientRepository: RecipientRepository - - private val gson = App.appCore.gson + private val accountRepository = AccountRepository(session.walletStorage.database.accountDao()) + private val transferRepository = TransferRepository(session.walletStorage.database.transferDao()) + private val identityRepository = IdentityRepository(session.walletStorage.database.identityDao()) + private val recipientRepository = RecipientRepository(session.walletStorage.database.recipientDao()) private lateinit var transactionMappingHelper: TransactionMappingHelper private val accountUpdater = AccountUpdater(application, viewModelScope) @@ -111,14 +102,6 @@ class AccountDetailsViewModel(application: Application) : AndroidViewModel(appli get() = _accountUpdatedLiveData init { - val accountDao = WalletDatabase.getDatabase(application).accountDao() - accountRepository = AccountRepository(accountDao) - val transferDao = WalletDatabase.getDatabase(application).transferDao() - transferRepository = TransferRepository(transferDao) - val identityDao = WalletDatabase.getDatabase(application).identityDao() - identityRepository = IdentityRepository(identityDao) - val recipientDao = WalletDatabase.getDatabase(application).recipientDao() - recipientRepository = RecipientRepository(recipientDao) initializeAccountUpdater() _transferListLiveData.observeForever { @@ -366,7 +349,7 @@ class AccountDetailsViewModel(application: Application) : AndroidViewModel(appli } private fun addToTransactionList(newTransactions: List) { - val transferList = _transferListLiveData.value ?: ArrayList() + val transferList = _transferListLiveData.value ?: ArrayList() val adapterList = transferList.toMutableList() for (ta in newTransactions) { val isAfterHeader = checkToAddHeaderItem(adapterList, ta) diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountSettingsViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountSettingsViewModel.kt index ae0399f8..08b06d4f 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountSettingsViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountSettingsViewModel.kt @@ -4,10 +4,10 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.concordium.wallet.App import com.concordium.wallet.data.AccountRepository import com.concordium.wallet.data.RecipientRepository import com.concordium.wallet.data.room.Account -import com.concordium.wallet.data.room.WalletDatabase import kotlinx.coroutines.launch class AccountSettingsViewModel(application: Application) : AndroidViewModel(application) { @@ -21,9 +21,9 @@ class AccountSettingsViewModel(application: Application) : AndroidViewModel(appl fun changeAccountName(name: String) { val accountRepository = - AccountRepository(WalletDatabase.getDatabase(getApplication()).accountDao()) + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) val recipientRepository = - RecipientRepository(WalletDatabase.getDatabase(getApplication()).recipientDao()) + RecipientRepository(App.appCore.session.walletStorage.database.recipientDao()) viewModelScope.launch { account.name = name accountRepository.update(account) diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountTransactionsFiltersActivity.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountTransactionsFiltersActivity.kt index a7956926..4fede94e 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountTransactionsFiltersActivity.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountTransactionsFiltersActivity.kt @@ -3,7 +3,7 @@ package com.concordium.wallet.ui.account.accountdetails import android.os.Bundle import com.concordium.wallet.App import com.concordium.wallet.R -import com.concordium.wallet.core.authentication.Session +import com.concordium.wallet.core.Session import com.concordium.wallet.data.room.Account import com.concordium.wallet.databinding.ActivityAccountTransactionFiltersBinding import com.concordium.wallet.ui.base.BaseActivity diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewFragment.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewFragment.kt index 679cc20d..cfb35b68 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewFragment.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewFragment.kt @@ -20,7 +20,6 @@ import com.concordium.wallet.App import com.concordium.wallet.R import com.concordium.wallet.core.arch.EventObserver import com.concordium.wallet.data.model.Token -import com.concordium.wallet.data.preferences.AuthPreferences import com.concordium.wallet.data.preferences.Preferences import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.util.CurrencyUtil @@ -36,11 +35,12 @@ import com.concordium.wallet.ui.base.BaseActivity import com.concordium.wallet.ui.base.BaseFragment import com.concordium.wallet.ui.cis2.SendTokenActivity import com.concordium.wallet.ui.more.export.ExportActivity +import com.concordium.wallet.ui.multiwallet.FileWalletCreationLimitationDialog +import com.concordium.wallet.ui.multiwallet.WalletsActivity import com.concordium.wallet.ui.onboarding.OnboardingFragment import com.concordium.wallet.ui.onboarding.OnboardingSharedViewModel import com.concordium.wallet.ui.onboarding.OnboardingState import com.concordium.wallet.ui.onramp.CcdOnrampSitesActivity -import com.concordium.wallet.util.KeyCreationVersion import com.concordium.wallet.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -58,7 +58,6 @@ class AccountsOverviewFragment : BaseFragment() { private lateinit var viewModel: AccountsOverviewViewModel private lateinit var mainViewModel: MainViewModel private lateinit var onboardingViewModel: OnboardingSharedViewModel - private lateinit var keyCreationVersion: KeyCreationVersion private lateinit var onboardingStatusCard: OnboardingFragment //region Lifecycle @@ -68,7 +67,6 @@ class AccountsOverviewFragment : BaseFragment() { super.onCreate(savedInstanceState) setHasOptionsMenu(true) - keyCreationVersion = KeyCreationVersion(AuthPreferences(requireContext())) } override fun onCreateView( @@ -91,8 +89,13 @@ class AccountsOverviewFragment : BaseFragment() { val baseActivity = (activity as BaseActivity) - baseActivity.hideLeftPlus(isVisible = true) { - if (mainViewModel.hasCompletedOnboarding()) { + baseActivity.hideLeftPlus( + isVisible = true, + hasNotice = viewModel.isCreationLimitedForFileWallet, + ) { + if (viewModel.isCreationLimitedForFileWallet) { + showFileWalletCreationLimitation() + } else if (mainViewModel.hasCompletedOnboarding()) { gotoCreateAccount() } else { baseActivity.showUnlockFeatureDialog() @@ -198,7 +201,7 @@ class AccountsOverviewFragment : BaseFragment() { } viewModel.onrampActive.collectWhenStarted(viewLifecycleOwner) { active -> if (active) { - if (!viewModel.hasShowedInitialAnimation()) { + if (!viewModel.hasShownInitialAnimation()) { binding.confettiAnimation.visibility = View.VISIBLE binding.confettiAnimation.playAnimation() } @@ -225,6 +228,11 @@ class AccountsOverviewFragment : BaseFragment() { onboardingViewModel.setIdentity(identity) } + viewModel.fileWalletMigrationVisible.collectWhenStarted( + viewLifecycleOwner, + binding.fileWalletMigrationDisclaimerLayout::isVisible::set + ) + onboardingViewModel.identityFlow.collectWhenStarted(viewLifecycleOwner) { identity -> onboardingStatusCard.updateViewsByIdentityStatus(identity) } @@ -264,6 +272,10 @@ class AccountsOverviewFragment : BaseFragment() { App.appCore.session.addAccountsBackedUpListener(it) } + binding.fileWalletMigrationDisclaimerLayout.setOnClickListener { + startActivity(Intent(requireActivity(), WalletsActivity::class.java)) + } + initializeAnimation() initializeList() updateMissingBackup() @@ -271,7 +283,7 @@ class AccountsOverviewFragment : BaseFragment() { private fun updateMissingBackup() = viewModel.viewModelScope.launch(Dispatchers.Main) { binding.missingBackup.isVisible = App.appCore.session.run { - isAccountsBackupPossible() && !isAccountsBackedUp() + isAccountsBackupPossible() && !areAccountsBackedUp() } } @@ -389,6 +401,13 @@ class AccountsOverviewFragment : BaseFragment() { startActivity(intent) } + private fun showFileWalletCreationLimitation() { + FileWalletCreationLimitationDialog().showSingle( + childFragmentManager, + FileWalletCreationLimitationDialog.TAG + ) + } + private fun showWaiting(waiting: Boolean) { if (waiting) { binding.progress.progressLayout.visibility = View.VISIBLE @@ -439,7 +458,7 @@ class AccountsOverviewFragment : BaseFragment() { } private fun cancelAnimation() { - viewModel.setHasShowedInitialAnimation() + viewModel.setHasShownInitialAnimation() binding.confettiAnimation.visibility = View.GONE } diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewViewModel.kt index 124889af..6e2dfb5b 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewViewModel.kt @@ -10,21 +10,18 @@ import androidx.lifecycle.viewModelScope import com.concordium.wallet.App import com.concordium.wallet.BuildConfig import com.concordium.wallet.core.arch.Event +import com.concordium.wallet.core.multiwallet.AppWallet import com.concordium.wallet.core.notifications.UpdateNotificationsSubscriptionUseCase import com.concordium.wallet.data.AccountRepository import com.concordium.wallet.data.IdentityRepository import com.concordium.wallet.data.model.TransactionStatus -import com.concordium.wallet.data.preferences.AuthPreferences -import com.concordium.wallet.data.preferences.NotificationsPreferences import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.room.AccountWithIdentity import com.concordium.wallet.data.room.Identity -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.ui.account.common.accountupdater.AccountUpdater import com.concordium.wallet.ui.account.common.accountupdater.TotalBalancesData import com.concordium.wallet.ui.onboarding.OnboardingState import com.concordium.wallet.ui.onramp.CcdOnrampSiteRepository -import com.concordium.wallet.util.KeyCreationVersion import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -52,6 +49,9 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app private val _onrampActive = MutableStateFlow(false) val onrampActive = _onrampActive.asStateFlow() + private val _fileWalletMigrationVisible = MutableStateFlow(false) + val fileWalletMigrationVisible = _fileWalletMigrationVisible.asSharedFlow() + private val zeroAccountBalances = TotalBalancesData( BigInteger.ZERO, BigInteger.ZERO, @@ -70,29 +70,24 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app private val _listItemsLiveData = MutableLiveData>() val listItemsLiveData: LiveData> = _listItemsLiveData - private val identityRepository: IdentityRepository - private val accountRepository: AccountRepository + private val identityRepository = + IdentityRepository(App.appCore.session.walletStorage.database.identityDao()) + private val accountRepository = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) private val accountUpdater = AccountUpdater(application, viewModelScope) - private val keyCreationVersion: KeyCreationVersion private val ccdOnrampSiteRepository: CcdOnrampSiteRepository private val accountsObserver: Observer> - private val notificationsPreferences: NotificationsPreferences + private val updateNotificationsSubscriptionUseCase by lazy(::UpdateNotificationsSubscriptionUseCase) + val isCreationLimitedForFileWallet: Boolean + get() = App.appCore.session.activeWallet.type == AppWallet.Type.FILE private var updater: CountDownTimer? = null - private val updateNotificationsSubscriptionUseCase by lazy { - UpdateNotificationsSubscriptionUseCase(application) - } - enum class DialogToShow { UNSHIELDING, ; } init { - val identityDao = WalletDatabase.getDatabase(application).identityDao() - identityRepository = IdentityRepository(identityDao) - val accountDao = WalletDatabase.getDatabase(application).accountDao() - accountRepository = AccountRepository(accountDao) accountUpdater.setUpdateListener(object : AccountUpdater.UpdateListener { override fun onDone(totalBalances: TotalBalancesData) { _waitingLiveData.postValue(false) @@ -115,13 +110,12 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app _errorLiveData.postValue(Event(stringRes)) } }) - keyCreationVersion = KeyCreationVersion(AuthPreferences(application)) ccdOnrampSiteRepository = CcdOnrampSiteRepository() accountsObserver = Observer { accountsWithIdentity -> postListItems(accountsWithIdentity) } accountRepository.allAccountsWithIdentity.observeForever(accountsObserver) - notificationsPreferences = NotificationsPreferences(application) + _fileWalletMigrationVisible.tryEmit(App.appCore.session.activeWallet.type == AppWallet.Type.FILE) updateNotificationSubscription() } @@ -131,31 +125,36 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app accountUpdater.dispose() } - fun updateState(notifyWaitingLiveData: Boolean = true) { + fun updateState(notifyWaitingLiveData: Boolean = true) = viewModelScope.launch(Dispatchers.IO) { // Decide what state to show (visible buttons based on if there is any identities and accounts) // Also update all accounts (and set the overall balance) if any exists. - viewModelScope.launch(Dispatchers.IO) { - if (!keyCreationVersion.useV1) { - checkFileWallet(notifyWaitingLiveData) - } else { - val identityCount = identityRepository.getCount() - if (identityCount == 0) { - postState( - OnboardingState.VERIFY_IDENTITY, - zeroAccountBalances, - notifyWaitingLiveData - ) - } else { - updateIdentityStatus(notifyWaitingLiveData) - } - } + + if (App.appCore.session.activeWallet.type == AppWallet.Type.FILE) { + postState(OnboardingState.DONE, notifyWaitingLiveData = notifyWaitingLiveData) + updateSubmissionStatesAndBalances() + return@launch + } + + when { + !App.appCore.session.walletStorage.setupPreferences.hasEncryptedSeed() -> + postState(OnboardingState.SAVE_PHRASE, zeroAccountBalances, notifyWaitingLiveData) + + identityRepository.getCount() == 0 -> + postState( + OnboardingState.VERIFY_IDENTITY, + zeroAccountBalances, + notifyWaitingLiveData + ) + + else -> + updateIdentityStatus(notifyWaitingLiveData) } } private suspend fun postState( state: OnboardingState, balances: TotalBalancesData = zeroAccountBalances, - notifyWaitingLiveData: Boolean = true + notifyWaitingLiveData: Boolean = true, ) { _stateFlow.emit(state) _totalBalanceLiveData.postValue(balances) @@ -193,22 +192,10 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app } } - private suspend fun checkFileWallet(notifyWaitingLiveData: Boolean) { - val doneCount = identityRepository.getAllDone().size - if (doneCount > 0) { - App.appCore.session.hasCompletedOnboarding() - postState(OnboardingState.DONE, notifyWaitingLiveData = notifyWaitingLiveData) - showSingleDialogIfNeeded() - updateSubmissionStatesAndBalances() - } else { - postState(OnboardingState.SAVE_PHRASE, zeroAccountBalances, notifyWaitingLiveData) - } - } - private suspend fun handleAccountCreation(notifyWaitingLiveData: Boolean) { val allAccounts = accountRepository.getAll() if (allAccounts.any { it.transactionStatus == TransactionStatus.FINALIZED }) { - App.appCore.session.hasCompletedOnboarding() + App.appCore.session.walletStorage.setupPreferences.setHasCompletedOnboarding(true) postState(OnboardingState.DONE, notifyWaitingLiveData = notifyWaitingLiveData) showSingleDialogIfNeeded() } else { @@ -237,12 +224,12 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app updater = null } - fun hasShowedInitialAnimation(): Boolean { - return App.appCore.session.getHasShowedInitialAnimation() + fun hasShownInitialAnimation(): Boolean { + return App.appCore.session.walletStorage.setupPreferences.getHasShownInitialAnimation() } - fun setHasShowedInitialAnimation() { - App.appCore.session.setHasShowedInitialAnimation() + fun setHasShownInitialAnimation() { + return App.appCore.session.walletStorage.setupPreferences.setHasShownInitialAnimation(true) } private fun startUpdater(countdownInterval: Long = BuildConfig.ACCOUNT_UPDATE_FREQUENCY_SEC) { @@ -278,11 +265,6 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app return false } - fun checkUsingV1KeyCreation() = - check(keyCreationVersion.useV1) { - "Key creation V1 (seed-based) must be used to perform this action" - } - private fun showSingleDialogIfNeeded() = viewModelScope.launch(Dispatchers.IO) { val dialogsToShow = linkedSetOf() diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/UnshieldingNoticeDialog.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/UnshieldingNoticeDialog.kt index c9b580b8..87d1d6ee 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/UnshieldingNoticeDialog.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/UnshieldingNoticeDialog.kt @@ -40,7 +40,7 @@ class UnshieldingNoticeDialog : AppCompatDialogFragment() { // Track showing the notice once it is visible to the user. viewLifecycleOwner.lifecycleScope.launchWhenStarted { delay(500) - App.appCore.session.unshieldingNoticeShown() + App.appCore.session.setUnshieldingNoticeShown() } } diff --git a/app/src/main/java/com/concordium/wallet/ui/account/common/NewAccountViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/common/NewAccountViewModel.kt index a430aef6..6df25a03 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/common/NewAccountViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/common/NewAccountViewModel.kt @@ -13,31 +13,24 @@ import com.concordium.wallet.core.backend.BackendError import com.concordium.wallet.core.backend.BackendErrorException import com.concordium.wallet.core.backend.BackendRequest import com.concordium.wallet.data.AccountRepository -import com.concordium.wallet.data.IdentityRepository import com.concordium.wallet.data.RecipientRepository import com.concordium.wallet.data.backend.repository.IdentityProviderRepository import com.concordium.wallet.data.backend.repository.ProxyRepository -import com.concordium.wallet.data.cryptolib.CreateCredentialInput import com.concordium.wallet.data.cryptolib.CreateCredentialInputV1 import com.concordium.wallet.data.cryptolib.CreateCredentialOutput -import com.concordium.wallet.data.cryptolib.GenerateAccountsInput import com.concordium.wallet.data.cryptolib.StorageAccountData import com.concordium.wallet.data.model.AccountSubmissionStatus import com.concordium.wallet.data.model.CredentialWrapper +import com.concordium.wallet.data.model.EncryptedData import com.concordium.wallet.data.model.GlobalParams import com.concordium.wallet.data.model.GlobalParamsWrapper -import com.concordium.wallet.data.model.PossibleAccount -import com.concordium.wallet.data.model.RawJson import com.concordium.wallet.data.model.SubmissionData import com.concordium.wallet.data.model.TransactionStatus -import com.concordium.wallet.data.preferences.AuthPreferences import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.room.Identity import com.concordium.wallet.data.room.Recipient -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.ui.common.BackendErrorHandler import com.concordium.wallet.util.DateTimeUtil -import com.concordium.wallet.util.KeyCreationVersion import com.concordium.wallet.util.Log import com.google.gson.JsonArray import kotlinx.coroutines.Dispatchers @@ -48,11 +41,11 @@ open class NewAccountViewModel(application: Application) : private val identityProviderRepository = IdentityProviderRepository() private val proxyRepository = ProxyRepository() - private val identityRepository: IdentityRepository - private val accountRepository: AccountRepository - private val recipientRepository: RecipientRepository + private val accountRepository: AccountRepository = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) + private val recipientRepository: RecipientRepository = + RecipientRepository(App.appCore.session.walletStorage.database.recipientDao()) private val gson = App.appCore.gson - private val keyCreationVersion = KeyCreationVersion(AuthPreferences(App.appContext)) private var globalParamsRequest: BackendRequest? = null private var submitCredentialRequest: BackendRequest? = null @@ -91,20 +84,11 @@ open class NewAccountViewModel(application: Application) : var globalParams: GlobalParams? = null var submissionId: String? = null var accountAddress: String? = null - var encryptedAccountData: String? = null + var encryptedAccountData: EncryptedData? = null var credential: CredentialWrapper? = null var nextCredNumber: Int? = null } - init { - val identityDao = WalletDatabase.getDatabase(application).identityDao() - identityRepository = IdentityRepository(identityDao) - val accountDao = WalletDatabase.getDatabase(application).accountDao() - accountRepository = AccountRepository(accountDao) - val recipientDao = WalletDatabase.getDatabase(application).recipientDao() - recipientRepository = RecipientRepository(recipientDao) - } - override fun onCleared() { super.onCleared() globalParamsRequest?.dispose() @@ -131,6 +115,7 @@ open class NewAccountViewModel(application: Application) : globalParamsRequest?.dispose() globalParamsRequest = identityProviderRepository.getIGlobalInfo( { + App.appCore.session.walletStorage.setupPreferences.setHasCompletedOnboarding(true) tempData.globalParams = it.value _showAuthenticationLiveData.postValue(Event(true)) _waitingLiveData.postValue(false) @@ -149,38 +134,19 @@ open class NewAccountViewModel(application: Application) : private suspend fun decryptAndContinue(password: String) { val globalParams = tempData.globalParams - if (keyCreationVersion.useV1) { - // Proceed to credentials creation as there is nothing to decrypt. - if (globalParams == null) { - _errorLiveData.value = Event(R.string.app_error_general) - _waitingLiveData.value = false - return - } - createCredentials(password, null, globalParams) - } else { - // Decrypt the private data. - val privateIdObjectDataEncrypted = identity.privateIdObjectDataEncrypted - if (globalParams == null) { - _errorLiveData.postValue(Event(R.string.app_error_general)) - _waitingLiveData.postValue(false) - return - } - val decryptedJson = - App.appCore.getCurrentAuthenticationManager() - .decryptInBackground(password, privateIdObjectDataEncrypted) - if (decryptedJson != null) { - val privateIdObjectData = gson.fromJson(decryptedJson, RawJson::class.java) - createCredentials(password, privateIdObjectData, globalParams) - } else { - _errorLiveData.postValue(Event(R.string.app_error_encryption)) - _waitingLiveData.postValue(false) - } + + // Proceed to credentials creation as there is nothing to decrypt. + if (globalParams == null) { + _errorLiveData.value = Event(R.string.app_error_general) + _waitingLiveData.value = false + return } + + createCredentials(password, globalParams) } private suspend fun createCredentials( password: String, - privateIdObjectData: RawJson?, globalParams: GlobalParams ) { val identityProvider = identity.identityProvider @@ -197,49 +163,24 @@ open class NewAccountViewModel(application: Application) : tempData.nextCredNumber = accountRepository.nextCredNumber(identity.id) - val output: CreateCredentialOutput? = - if (keyCreationVersion.useV1) { - val net = AppConfig.net - val seed = AuthPreferences(getApplication()).getSeedHex(password) - - val credentialInput = CreateCredentialInputV1( - ipInfo = idProviderInfo, - arsInfos = arsInfos, - global = globalParams, - identityObject = identityObject, - revealedAttributes = JsonArray(), - seed = seed, - net = net, - identityIndex = identity.identityIndex, - accountNumber = tempData.nextCredNumber ?: 0, - expiry = (DateTimeUtil.nowPlusMinutes(5).time) / 1000, - ) - - App.appCore.cryptoLibrary.createCredentialV1(credentialInput) - } else { - requireNotNull(privateIdObjectData) { - "Private ID data must be set when using V0 creation" - } - - val generateAccountsInput = - GenerateAccountsInput(globalParams, identityObject, privateIdObjectData) - val nextAccountNumber = findNextAccountNumber(generateAccountsInput) - ?: // Error has been handled - return - - val credentialInput = CreateCredentialInput( - ipInfo = idProviderInfo, - arsInfos = arsInfos, - global = globalParams, - identityObject = identityObject, - privateIdObjectData = privateIdObjectData, - revealedAttributes = JsonArray(), - accountNumber = nextAccountNumber, - expiry = (DateTimeUtil.nowPlusMinutes(5).time) / 1000, - ) + val net = AppConfig.net + val seed = App.appCore.session.walletStorage.setupPreferences.getSeedHex(password) + + val credentialInput = CreateCredentialInputV1( + ipInfo = idProviderInfo, + arsInfos = arsInfos, + global = globalParams, + identityObject = identityObject, + revealedAttributes = JsonArray(), + seed = seed, + net = net, + identityIndex = identity.identityIndex, + accountNumber = tempData.nextCredNumber ?: 0, + expiry = (DateTimeUtil.nowPlusMinutes(5).time) / 1000, + ) - App.appCore.cryptoLibrary.createCredential(credentialInput) - } + val output: CreateCredentialOutput? = + App.appCore.cryptoLibrary.createCredentialV1(credentialInput) if (output == null) { _errorLiveData.postValue(Event(R.string.app_error_lib)) @@ -255,8 +196,11 @@ open class NewAccountViewModel(application: Application) : ) ) - val storageAccountDataEncrypted = App.appCore.getCurrentAuthenticationManager() - .encryptInBackground(password, jsonToBeEncrypted) + val storageAccountDataEncrypted = App.appCore.auth + .encrypt( + password = password, + data = jsonToBeEncrypted.toByteArray() + ) if (storageAccountDataEncrypted != null) { tempData.encryptedAccountData = storageAccountDataEncrypted tempData.credential = output.credential @@ -268,77 +212,6 @@ open class NewAccountViewModel(application: Application) : } } - private suspend fun findNextAccountNumber(generateAccountsInput: GenerateAccountsInput): Int? { - var nextAccountNumber = identity.nextAccountNumber - Log.d("nextAccountNumber: $nextAccountNumber") - val maxAccounts = identity.identityObject!!.attributeList.maxAccounts - - val possibleAccountList = App.appCore.cryptoLibrary.generateAccounts(generateAccountsInput) - if (possibleAccountList == null) { - Log.e("Could not generate accounts, so do not allow account creation") - _errorLiveData.postValue(Event(R.string.app_error_lib)) - _waitingLiveData.postValue(false) - return null - } else { - Log.d("Generated account info for ${possibleAccountList.size} accounts") - val next = checkExistingAccounts(possibleAccountList, nextAccountNumber) - if (next == null) { - _errorLiveData.postValue(Event(R.string.app_error_backend_unknown)) - _waitingLiveData.postValue(false) - return null - } else { - nextAccountNumber = next - } - } - - Log.d("nextAccountNumber used: $nextAccountNumber") - // Next account number starts from 0 - if (nextAccountNumber >= maxAccounts) { - _errorLiveData.postValue(Event(R.string.new_account_identity_attributes_error_max_accounts_alt)) - _waitingLiveData.postValue(false) - return null - } - identity.nextAccountNumber = nextAccountNumber + 1 - viewModelScope.launch(Dispatchers.IO) { - identityRepository.update(identity) - } - return nextAccountNumber - } - - /** - * Returns the index for the first unused account - */ - private suspend fun checkExistingAccounts( - possibleAccountList: List, - startIndex: Int - ): Int? { - var index = startIndex - while (index < possibleAccountList.size) { - try { - Log.d("Get account balance for index $index") - val accountBalance = - proxyRepository.getAccountBalanceSuspended(possibleAccountList[index].accountAddress) - Log.d("AccountBalance: $accountBalance") - if (!accountBalance.accountExists()) { - Log.d("Unused account address") - return index - } - index++ - } catch (e: Exception) { - val ex = BackendErrorHandler.getCoroutineBackendException(e) - return if (ex != null && ex is BackendErrorException) { - Log.d("Backend error - unused account address", ex) - index - } else { - // Other exceptions like connection problems should not let the account be created - Log.e("Unexpected exception when getting account balance", e) - null - } - } - } - return null - } - private fun submitCredential(credentialWrapper: CredentialWrapper) { _waitingLiveData.postValue(true) submitCredentialRequest?.dispose() diff --git a/app/src/main/java/com/concordium/wallet/ui/account/common/accountupdater/AccountUpdater.kt b/app/src/main/java/com/concordium/wallet/ui/account/common/accountupdater/AccountUpdater.kt index f058f71c..200b5465 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/common/accountupdater/AccountUpdater.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/common/accountupdater/AccountUpdater.kt @@ -25,7 +25,6 @@ import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.room.EncryptedAmount import com.concordium.wallet.data.room.Recipient import com.concordium.wallet.data.room.Transfer -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.ui.cis2.defaults.DefaultFungibleTokensManager import com.concordium.wallet.ui.cis2.defaults.DefaultTokensManagerFactory import com.concordium.wallet.ui.common.BackendErrorHandler @@ -60,10 +59,14 @@ class AccountUpdater(val application: Application, private val viewModelScope: C ) private val proxyRepository = ProxyRepository() - private val accountRepository: AccountRepository - private val encryptedAmountRepository: EncryptedAmountRepository - private val transferRepository: TransferRepository - private val recipientRepository: RecipientRepository + private val accountRepository = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) + private val encryptedAmountRepository = + EncryptedAmountRepository(App.appCore.session.walletStorage.database.encryptedAmountDao()) + private val transferRepository = + TransferRepository(App.appCore.session.walletStorage.database.transferDao()) + private val recipientRepository = + RecipientRepository(App.appCore.session.walletStorage.database.recipientDao()) private val defaultFungibleTokensManager: DefaultFungibleTokensManager private var accountSubmissionStatusRequestList: MutableList = @@ -79,23 +82,12 @@ class AccountUpdater(val application: Application, private val viewModelScope: C private var transferList: MutableList = ArrayList() private var transfersToDeleteList: MutableList = ArrayList() - private val updateNotificationsSubscriptionUseCase by lazy { - UpdateNotificationsSubscriptionUseCase(application) - } + private val updateNotificationsSubscriptionUseCase by lazy(::UpdateNotificationsSubscriptionUseCase) init { - val accountDao = WalletDatabase.getDatabase(application).accountDao() - accountRepository = AccountRepository(accountDao) - val transferDao = WalletDatabase.getDatabase(application).transferDao() - transferRepository = TransferRepository(transferDao) - val encryptedAmountDao = WalletDatabase.getDatabase(application).encryptedAmountDao() - encryptedAmountRepository = EncryptedAmountRepository(encryptedAmountDao) - val recipientDao = WalletDatabase.getDatabase(application).recipientDao() - recipientRepository = RecipientRepository(recipientDao) - - val contractTokenDao = WalletDatabase.getDatabase(application).contractTokenDao() + val defaultTokensManagerFactory = DefaultTokensManagerFactory( - contractTokensRepository = ContractTokensRepository(contractTokenDao), + contractTokensRepository = ContractTokensRepository(App.appCore.session.walletStorage.database.contractTokenDao()), ) defaultFungibleTokensManager = defaultTokensManagerFactory.getDefaultFungibleTokensManager() } diff --git a/app/src/main/java/com/concordium/wallet/ui/account/newaccountconfirmed/NewAccountConfirmedViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/newaccountconfirmed/NewAccountConfirmedViewModel.kt index 2c61b42d..fff88b7d 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/newaccountconfirmed/NewAccountConfirmedViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/newaccountconfirmed/NewAccountConfirmedViewModel.kt @@ -1,18 +1,20 @@ package com.concordium.wallet.ui.account.newaccountconfirmed import android.app.Application -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.concordium.wallet.App import com.concordium.wallet.data.AccountRepository -import com.concordium.wallet.data.IdentityRepository import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.room.AccountWithIdentity -import com.concordium.wallet.data.room.WalletDatabase import kotlinx.coroutines.launch class NewAccountConfirmedViewModel(application: Application) : AndroidViewModel(application) { - private val identityRepository: IdentityRepository - private val accountRepository: AccountRepository + private val accountRepository = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) private val _waitingLiveData = MutableLiveData() val waitingLiveData: LiveData @@ -22,13 +24,6 @@ class NewAccountConfirmedViewModel(application: Application) : AndroidViewModel( lateinit var accountWithIdentityLiveData: LiveData - init { - val identityDao = WalletDatabase.getDatabase(application).identityDao() - identityRepository = IdentityRepository(identityDao) - val accountDao = WalletDatabase.getDatabase(application).accountDao() - accountRepository = AccountRepository(accountDao) - } - fun initialize(account: Account) { this.account = account accountWithIdentityLiveData = accountRepository.getByIdWithIdentityAsLiveData(account.id) @@ -40,4 +35,4 @@ class NewAccountConfirmedViewModel(application: Application) : AndroidViewModel( _waitingLiveData.value = false } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/concordium/wallet/ui/account/newaccountidentity/NewAccountIdentityViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/newaccountidentity/NewAccountIdentityViewModel.kt index 1001c6d7..3f2f679e 100644 --- a/app/src/main/java/com/concordium/wallet/ui/account/newaccountidentity/NewAccountIdentityViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/account/newaccountidentity/NewAccountIdentityViewModel.kt @@ -4,19 +4,19 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.concordium.wallet.App import com.concordium.wallet.R import com.concordium.wallet.core.arch.Event import com.concordium.wallet.data.IdentityRepository import com.concordium.wallet.data.room.Identity -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.util.Log class NewAccountIdentityViewModel(application: Application) : AndroidViewModel(application) { lateinit var accountName: String - private val identityRepository: IdentityRepository - val identityListLiveData: LiveData> + private val identityRepository = IdentityRepository(App.appCore.session.walletStorage.database.identityDao()) + val identityListLiveData = identityRepository.allDoneIdentities private val _errorLiveData = MutableLiveData>() val errorLiveData: LiveData> @@ -26,12 +26,6 @@ class NewAccountIdentityViewModel(application: Application) : AndroidViewModel(a val errorDialogLiveData: LiveData> get() = _errorDialogLiveData - init { - val identityDao = WalletDatabase.getDatabase(application).identityDao() - identityRepository = IdentityRepository(identityDao) - identityListLiveData = identityRepository.allDoneIdentities - } - fun initialize(accountName: String) { this.accountName = accountName } @@ -47,4 +41,4 @@ class NewAccountIdentityViewModel(application: Application) : AndroidViewModel(a } return true } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/concordium/wallet/ui/airdrop/AirDropViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/airdrop/AirDropViewModel.kt index bb89bbc6..c8c1b4dd 100644 --- a/app/src/main/java/com/concordium/wallet/ui/airdrop/AirDropViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/airdrop/AirDropViewModel.kt @@ -5,12 +5,12 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.concordium.wallet.App import com.concordium.wallet.data.AccountRepository import com.concordium.wallet.data.backend.airdrop.AirDropRepository import com.concordium.wallet.data.backend.airdrop.RegistrationRequest import com.concordium.wallet.data.backend.airdrop.RegistrationResponse import com.concordium.wallet.data.room.Account -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.extension.Event import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -36,12 +36,8 @@ class AirDropViewModel(application: Application) : AndroidViewModel(application) private val _doRegistrationResponseLiveData: MutableLiveData> = MutableLiveData() fun doRegistrationResponse(): LiveData> = _doRegistrationResponseLiveData - private var accountRepository: AccountRepository? = null - - init { - val accountDao = WalletDatabase.getDatabase(application).accountDao() - accountRepository = AccountRepository(accountDao) - } + private val accountRepository = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) fun processAction(action: AirDropAction) { when (action) { @@ -79,4 +75,4 @@ class AirDropViewModel(application: Application) : AndroidViewModel(application) _walletsLiveData.postValue(Event(wallets)) _viewState.emit(AirDropState.SelectWallet) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/login/AuthLoginViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/login/AuthLoginViewModel.kt index d1fead62..fa224870 100644 --- a/app/src/main/java/com/concordium/wallet/ui/auth/login/AuthLoginViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/auth/login/AuthLoginViewModel.kt @@ -7,8 +7,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.concordium.wallet.App import com.concordium.wallet.R +import com.concordium.wallet.core.Session import com.concordium.wallet.core.arch.Event -import com.concordium.wallet.core.authentication.Session import com.concordium.wallet.core.security.KeystoreEncryptionException import kotlinx.coroutines.launch import javax.crypto.Cipher @@ -37,12 +37,13 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio } fun shouldShowBiometrics(): Boolean { - return App.appCore.getCurrentAuthenticationManager().useBiometrics() + return App.appCore.auth.isBiometricsUsed() } fun getCipherForBiometrics(): Cipher? { try { - val cipher = App.appCore.getCurrentAuthenticationManager().initBiometricsCipherForDecryption() + val cipher = + App.appCore.auth.getBiometricsCipherForDecryption() if (cipher == null) { _errorLiveData.value = Event(R.string.app_error_keystore_key_invalidated) } @@ -53,36 +54,33 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio } } - fun checkLogin(password: String) = viewModelScope.launch { + fun checkLogin(password: String?) = viewModelScope.launch { _waitingLiveData.value = true - val res = App.appCore.getCurrentAuthenticationManager().checkPasswordInBackground(password) - if (res) { + if (password != null && App.appCore.auth.checkPassword(password)) { loginSuccess() } else { _passwordErrorLiveData.value = - Event(if (App.appCore.getCurrentAuthenticationManager().usePasscode()) R.string.auth_login_passcode_error else R.string.auth_login_password_error) + Event( + if (App.appCore.auth + .isPasscodeUsed() + ) R.string.auth_login_passcode_error else R.string.auth_login_password_error + ) _waitingLiveData.value = false } } fun checkLogin(cipher: Cipher) = viewModelScope.launch { - _waitingLiveData.value = true - val password = App.appCore.getCurrentAuthenticationManager().checkPasswordInBackground(cipher) - if (password != null) { - loginSuccess() - } else { - _passwordErrorLiveData.value = - Event(if (App.appCore.getCurrentAuthenticationManager().usePasscode()) R.string.auth_login_passcode_error else R.string.auth_login_password_error) - _waitingLiveData.value = false - } + checkLogin( + password = App.appCore.auth.decryptPasswordWithBiometricsCipher(cipher), + ) } private fun loginSuccess() { - session.hasLoggedInUser() + session.setUserLoggedIn() _finishScreenLiveData.value = Event(true) } fun usePasscode(): Boolean { - return App.appCore.getCurrentAuthenticationManager().usePasscode() + return App.appCore.auth.isPasscodeUsed() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupActivity.kt deleted file mode 100644 index b547ea9d..00000000 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupActivity.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.concordium.wallet.ui.auth.setup - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.view.isVisible -import androidx.lifecycle.ViewModelProvider -import com.concordium.wallet.R -import com.concordium.wallet.core.arch.EventObserver -import com.concordium.wallet.databinding.ActivityAuthSetupBinding -import com.concordium.wallet.ui.auth.setupbiometrics.AuthSetupBiometricsActivity -import com.concordium.wallet.ui.auth.setuppassword.AuthSetupPasswordActivity -import com.concordium.wallet.ui.auth.setuprepeat.AuthSetupRepeatActivity -import com.concordium.wallet.ui.base.BaseActivity -import com.concordium.wallet.uicore.view.PasscodeView -import com.concordium.wallet.util.KeyboardUtil - -class AuthSetupActivity : BaseActivity( - R.layout.activity_auth_setup, - R.string.auth_setup_title -) { - - private var continueFlow: Boolean = true - - companion object { - const val CONTINUE_INITIAL_SETUP = "CONTINUE_INITIAL_SETUP" - } - - private lateinit var binding: ActivityAuthSetupBinding - private lateinit var viewModel: AuthSetupViewModel - - //region Lifecycle - //************************************************************ - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityAuthSetupBinding.bind(findViewById(R.id.root_layout)) - - continueFlow = intent.getBooleanExtra(CONTINUE_INITIAL_SETUP, true) - - initializeViewModel() - viewModel.initialize() - initializeViews() - - hideActionBarBack(isVisible = false) - } - - override fun onBackPressed() { - // Ignore back press - } - - private fun finishSuccess() { - setResult(Activity.RESULT_OK) - finish() - } - - //endregion - - //region Initialize - //************************************************************ - - private fun initializeViewModel() { - viewModel = ViewModelProvider( - this, - ViewModelProvider.AndroidViewModelFactory.getInstance(application) - )[AuthSetupViewModel::class.java] - viewModel.errorLiveData.observe(this, object : EventObserver() { - override fun onUnhandledEvent(value: Boolean) { - if (value) { - showPasswordError() - } - } - }) - viewModel.finishScreenLiveData.observe(this, object : EventObserver() { - override fun onUnhandledEvent(value: Boolean) { - if (value) { - finishSuccess() - } - } - }) - viewModel.gotoBiometricsSetupLiveData.observe(this, object : EventObserver() { - override fun onUnhandledEvent(value: Boolean) { - if (value) { - gotoAuthSetupBiometrics() - } - } - }) - } - - private fun initializeViews() { - binding.passcodeView.passcodeListener = object : PasscodeView.PasscodeListener { - override fun onInputChanged() { - binding.errorTextview.text = "" - binding.errorTextview.isVisible = false - } - - override fun onDone() { - onConfirmClicked() - } - } - binding.fullPasswordButton.setOnClickListener { - gotoAuthSetupPassword() - } - binding.passcodeView.requestFocus() - } - - //endregion - - //region Control/UI - //************************************************************ - - private fun onConfirmClicked() { - if (viewModel.checkPasswordRequirements(binding.passcodeView.getPasscode())) { - viewModel.startSetupPassword(binding.passcodeView.getPasscode()) - gotoAuthSetupPasscodeRepeat() - } else { - binding.passcodeView.clearPasscode() - binding.errorTextview.setText(R.string.auth_error_passcode_not_valid) - binding.errorTextview.isVisible = true - } - } - - private val getResultAuthSetupBiometrics = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - if (continueFlow) { - viewModel.hasFinishedSetupPassword() - } - finishSuccess() - } - } - - private fun gotoAuthSetupBiometrics() { - val intent = Intent(this, AuthSetupBiometricsActivity::class.java) - getResultAuthSetupBiometrics.launch(intent) - } - - private val getResultAuthSetupPasscodeRepeat = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - val result = it.data - if (it.resultCode == Activity.RESULT_OK && result != null) { - if (AuthSetupRepeatActivity.useFullPassword(result)) { - gotoAuthSetupPassword() - } else if (AuthSetupRepeatActivity.doesMatch(result)) { - viewModel.setupPassword(binding.passcodeView.getPasscode(), continueFlow) - } else { - binding.passcodeView.clearPasscode() - binding.errorTextview.setText(R.string.auth_error_passcodes_different) - binding.errorTextview.isVisible = true - } - } - } - - private fun gotoAuthSetupPasscodeRepeat() { - val intent = Intent(this, AuthSetupRepeatActivity::class.java) - getResultAuthSetupPasscodeRepeat.launch(intent) - } - - private val getResultAuthSetupFullPassword = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - if (continueFlow) { - viewModel.hasFinishedSetupPassword() - } - finishSuccess() - } - binding.passcodeView.clearPasscode() - } - - private fun gotoAuthSetupPassword() { - val intent = Intent(this, AuthSetupPasswordActivity::class.java) - getResultAuthSetupFullPassword.launch(intent) - } - - private fun showPasswordError() { - binding.passcodeView.clearPasscode() - KeyboardUtil.hideKeyboard(this) - popup.showSnackbar(binding.rootLayout, R.string.auth_error_password_setup) - } - - override fun loggedOut() { - } - - //endregion -} diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupBiometricsDialog.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsDialog.kt similarity index 86% rename from app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupBiometricsDialog.kt rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsDialog.kt index 7b222118..c61a9955 100644 --- a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupBiometricsDialog.kt +++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsDialog.kt @@ -1,4 +1,4 @@ -package com.concordium.wallet.ui.auth.passcode +package com.concordium.wallet.ui.auth.setup import android.content.DialogInterface import android.os.Bundle @@ -12,15 +12,14 @@ import androidx.lifecycle.ViewModelProvider import com.concordium.wallet.App import com.concordium.wallet.R import com.concordium.wallet.core.security.BiometricPromptCallback -import com.concordium.wallet.databinding.DialogPasscodeSetupBiometricsBinding -import com.concordium.wallet.ui.auth.setupbiometrics.AuthSetupBiometricsViewModel +import com.concordium.wallet.databinding.DialogAuthSetupBiometricsBinding import javax.crypto.Cipher -class PasscodeSetupBiometricsDialog : AppCompatDialogFragment() { +class AuthSetupBiometricsDialog : AppCompatDialogFragment() { override fun getTheme(): Int = R.style.CCX_Dialog - private lateinit var binding: DialogPasscodeSetupBiometricsBinding + private lateinit var binding: DialogAuthSetupBiometricsBinding private lateinit var viewModel: AuthSetupBiometricsViewModel private lateinit var biometricPrompt: BiometricPrompt @@ -29,7 +28,7 @@ class PasscodeSetupBiometricsDialog : AppCompatDialogFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = DialogPasscodeSetupBiometricsBinding.inflate(inflater, container, false) + binding = DialogAuthSetupBiometricsBinding.inflate(inflater, container, false) return binding.root } @@ -81,7 +80,7 @@ class PasscodeSetupBiometricsDialog : AppCompatDialogFragment() { val callback = object : BiometricPromptCallback() { override fun onAuthenticationSucceeded(cipher: Cipher) { - viewModel.setupBiometricWithPassword(cipher) + viewModel.proceedWithSetup(cipher) } } diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsViewModel.kt similarity index 58% rename from app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsViewModel.kt rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsViewModel.kt index 96220478..4701a3e3 100644 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsViewModel.kt @@ -1,4 +1,4 @@ -package com.concordium.wallet.ui.auth.setupbiometrics +package com.concordium.wallet.ui.auth.setup import android.app.Application import androidx.lifecycle.AndroidViewModel @@ -7,15 +7,11 @@ import androidx.lifecycle.MutableLiveData import com.concordium.wallet.App import com.concordium.wallet.R import com.concordium.wallet.core.arch.Event -import com.concordium.wallet.core.authentication.Session import com.concordium.wallet.core.security.KeystoreEncryptionException -import com.concordium.wallet.util.Log import javax.crypto.Cipher class AuthSetupBiometricsViewModel(application: Application) : AndroidViewModel(application) { - private val session: Session = App.appCore.session - private val _errorLiveData = MutableLiveData>() val errorLiveData: LiveData> get() = _errorLiveData @@ -24,8 +20,7 @@ class AuthSetupBiometricsViewModel(application: Application) : AndroidViewModel( get() = _finishScreenLiveData fun initialize() { - - val generated = App.appCore.getCurrentAuthenticationManager().generateBiometricsSecretKey() + val generated = App.appCore.auth.generateBiometricsSecretKey() if (!generated) { _errorLiveData.value = Event(R.string.app_error_keystore) } @@ -33,7 +28,7 @@ class AuthSetupBiometricsViewModel(application: Application) : AndroidViewModel( fun getCipherForBiometrics(): Cipher? { try { - val cipher = App.appCore.getCurrentAuthenticationManager().initBiometricsCipherForEncryption() + val cipher = App.appCore.auth.getBiometricsCipherForEncryption() if (cipher == null) { _errorLiveData.value = Event(R.string.app_error_keystore_key_invalidated) } @@ -44,17 +39,15 @@ class AuthSetupBiometricsViewModel(application: Application) : AndroidViewModel( } } - fun setupBiometricWithPassword(cipher: Cipher) { - val password = session.tempPassword - if (password != null) { - val setupDone = App.appCore.getCurrentAuthenticationManager().setupBiometrics(password, cipher) - if (setupDone) { - _finishScreenLiveData.value = Event(true) - } else { - _errorLiveData.value = Event(R.string.app_error_keystore) - } + fun proceedWithSetup(cipher: Cipher) { + val password = App.appCore.setup.authSetupPassword + ?: error("Setting up biometrics when there is no password to set up") + + val setupDone = App.appCore.auth.initBiometricAuth(password, cipher) + if (setupDone) { + _finishScreenLiveData.value = Event(true) } else { - Log.e("Temp password has been removed before biometrics was setup") + _errorLiveData.value = Event(R.string.app_error_keystore) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeActivity.kt similarity index 70% rename from app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupActivity.kt rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeActivity.kt index a2d43aaf..a41fb341 100644 --- a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupActivity.kt +++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeActivity.kt @@ -1,4 +1,4 @@ -package com.concordium.wallet.ui.auth.passcode +package com.concordium.wallet.ui.auth.setup import android.app.Activity import android.content.DialogInterface @@ -10,24 +10,24 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isInvisible import androidx.lifecycle.ViewModelProvider import com.concordium.wallet.R -import com.concordium.wallet.databinding.ActivityPasscodeSetupBinding +import com.concordium.wallet.databinding.ActivityAuthSetupPasscodeBinding import com.concordium.wallet.extension.collect import com.concordium.wallet.extension.collectWhenStarted -import com.concordium.wallet.ui.auth.setuppassword.AuthSetupPasswordActivity +import com.concordium.wallet.ui.auth.setup.password.AuthSetupPasswordActivity import com.concordium.wallet.ui.base.BaseActivity -class PasscodeSetupActivity : - BaseActivity(R.layout.activity_passcode_setup), +class AuthSetupPasscodeActivity : + BaseActivity(R.layout.activity_auth_setup_passcode), OnDismissListener { - private val binding: ActivityPasscodeSetupBinding by lazy { - ActivityPasscodeSetupBinding.bind(findViewById(R.id.toastLayoutTopError)) + private val binding: ActivityAuthSetupPasscodeBinding by lazy { + ActivityAuthSetupPasscodeBinding.bind(findViewById(R.id.toastLayoutTopError)) } - private val viewModel: PasscodeSetupViewModel by lazy { + private val viewModel: AuthSetupPasscodeViewModel by lazy { ViewModelProvider( this, ViewModelProvider.AndroidViewModelFactory.getInstance(application) - )[PasscodeSetupViewModel::class.java] + )[AuthSetupPasscodeViewModel::class.java] } private val getResultAuthSetupFullPassword = @@ -50,7 +50,7 @@ class PasscodeSetupActivity : length = viewModel.passcodeLength biometricsButton.isInvisible = true - mutableInput.observe(this@PasscodeSetupActivity) { inputValue -> + mutableInput.observe(this@AuthSetupPasscodeActivity) { inputValue -> if (inputValue.length == viewModel.passcodeLength) { viewModel.onPasscodeEntered(inputValue) } @@ -68,31 +68,31 @@ class PasscodeSetupActivity : private fun subscribeToState( ) = viewModel.stateFlow.collectWhenStarted(this) { state -> binding.titleTextView.text = when (state) { - is PasscodeSetupViewModel.State.Create -> getString(R.string.passcode_create_title) - PasscodeSetupViewModel.State.Repeat -> getString(R.string.passcode_repeat_title) + is AuthSetupPasscodeViewModel.State.Create -> getString(R.string.passcode_create_title) + AuthSetupPasscodeViewModel.State.Repeat -> getString(R.string.passcode_repeat_title) } binding.detailsTextView.text = when (state) { - is PasscodeSetupViewModel.State.Create -> getString( + is AuthSetupPasscodeViewModel.State.Create -> getString( R.string.template_passcode_create_details, viewModel.passcodeLength ) - PasscodeSetupViewModel.State.Repeat -> getString( + AuthSetupPasscodeViewModel.State.Repeat -> getString( R.string.template_passcode_repeat_details, viewModel.passcodeLength ) } when (state) { - is PasscodeSetupViewModel.State.Create -> { + is AuthSetupPasscodeViewModel.State.Create -> { binding.passcodeInputView.reset() if (state.hasError) { binding.passcodeInputView.animateError() } } - PasscodeSetupViewModel.State.Repeat -> { + AuthSetupPasscodeViewModel.State.Repeat -> { binding.passcodeInputView.reset() } } @@ -101,23 +101,23 @@ class PasscodeSetupActivity : private fun subscribeToEvents( ) = viewModel.eventsFlow.collect(this) { event -> when (event) { - PasscodeSetupViewModel.Event.FinishWithSuccess -> { + AuthSetupPasscodeViewModel.Event.FinishWithSuccess -> { setResult(Activity.RESULT_OK) finish() } - PasscodeSetupViewModel.Event.ShowFatalError -> { + AuthSetupPasscodeViewModel.Event.ShowFatalError -> { showError(R.string.passcode_setup_failed) } - PasscodeSetupViewModel.Event.SuggestBiometricsSetup -> { - PasscodeSetupBiometricsDialog().show( + AuthSetupPasscodeViewModel.Event.SuggestBiometricsSetup -> { + AuthSetupBiometricsDialog().show( supportFragmentManager, - PasscodeSetupBiometricsDialog.TAG + AuthSetupBiometricsDialog.TAG ) } - PasscodeSetupViewModel.Event.OpenFullPasswordSetUp -> + AuthSetupPasscodeViewModel.Event.OpenFullPasswordSetUp -> goToAuthSetupPassword() } } @@ -129,7 +129,6 @@ class PasscodeSetupActivity : private fun goToAuthSetupPassword() { val intent = Intent(this, AuthSetupPasswordActivity::class.java) - intent.putExtra(AuthSetupPasswordActivity.SKIP_BIOMETRICS, true) getResultAuthSetupFullPassword.launch(intent) } diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeViewModel.kt similarity index 67% rename from app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupViewModel.kt rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeViewModel.kt index a860e6e5..47c10117 100644 --- a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeViewModel.kt @@ -1,17 +1,16 @@ -package com.concordium.wallet.ui.auth.passcode +package com.concordium.wallet.ui.auth.setup import android.app.Application import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope import com.concordium.wallet.App -import com.concordium.wallet.core.authentication.Session import com.concordium.wallet.util.BiometricsUtil import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch -class PasscodeSetupViewModel(application: Application) : AndroidViewModel(application) { - private val session: Session = App.appCore.session - +class AuthSetupPasscodeViewModel(application: Application) : AndroidViewModel(application) { private val mutableStateFlow = MutableStateFlow( State.Create(hasError = false) ) @@ -29,7 +28,7 @@ class PasscodeSetupViewModel(application: Application) : AndroidViewModel(applic App.appCore.tracker.welcomePasscodeScreen() } - fun onPasscodeEntered(passcode: String) { + fun onPasscodeEntered(passcode: String) = viewModelScope.launch { require(passcode.length == passcodeLength) { "The entered passcode doesn't have the required length" } @@ -54,39 +53,48 @@ class PasscodeSetupViewModel(application: Application) : AndroidViewModel(applic } } - private fun proceedWithConfirmedCreatedPasscode() { + private suspend fun proceedWithConfirmedCreatedPasscode() { val confirmedPasscode = checkNotNull(createdPasscode) { "The passcode must be created at this point" } - session.startPasswordSetup(confirmedPasscode) - - val isSetUpSuccessfully = App.appCore - .getCurrentAuthenticationManager() - .createPasswordCheck(confirmedPasscode) + App.appCore.setup.beginAuthSetup(confirmedPasscode) + + val isSetUpSuccessfully = runCatching { + val authResetMasterKey = App.appCore.setup.authResetMasterKey + if (authResetMasterKey != null) { + App.appCore.auth.initPasswordAuth( + password = confirmedPasscode, + isPasscode = true, + masterKey = authResetMasterKey, + ) + } else { + App.appCore.auth.initPasswordAuth( + password = confirmedPasscode, + isPasscode = true, + ) + } + }.isSuccess if (isSetUpSuccessfully) { - onSetUpSuccessfully(usedPasscode = true) + onSetUpSuccessfully() } else { - session.hasFinishedSetupPassword() + App.appCore.setup.finishAuthSetup() mutableEventsFlow.tryEmit(Event.ShowFatalError) } } - private fun onSetUpSuccessfully(usedPasscode: Boolean) { - // Setting up password is done, so login screen should be shown next time app is opened - session.hasSetupPassword(usedPasscode) - + private fun onSetUpSuccessfully() { if (BiometricsUtil.isBiometricsAvailable()) { mutableEventsFlow.tryEmit(Event.SuggestBiometricsSetup) } else { - session.hasFinishedSetupPassword() + App.appCore.setup.finishAuthSetup() mutableEventsFlow.tryEmit(Event.FinishWithSuccess) } } fun onBiometricsSuggestionReviewed() { - session.hasFinishedSetupPassword() + App.appCore.setup.finishAuthSetup() mutableEventsFlow.tryEmit(Event.FinishWithSuccess) } @@ -100,7 +108,10 @@ class PasscodeSetupViewModel(application: Application) : AndroidViewModel(applic ) if (isSetUpSuccessfully) { - onSetUpSuccessfully(usedPasscode = false) + onSetUpSuccessfully() + } else { + App.appCore.setup.finishAuthSetup() + mutableEventsFlow.tryEmit(Event.ShowFatalError) } } diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupViewModel.kt deleted file mode 100644 index 9119f54c..00000000 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.concordium.wallet.ui.auth.setup - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.concordium.wallet.App -import com.concordium.wallet.core.arch.Event -import com.concordium.wallet.core.authentication.Session -import com.concordium.wallet.util.BiometricsUtil - -class AuthSetupViewModel(application: Application) : AndroidViewModel(application) { - - private val session: Session = App.appCore.session - - private val _errorLiveData = MutableLiveData>() - val errorLiveData: LiveData> - get() = _errorLiveData - private val _finishScreenLiveData = MutableLiveData>() - val finishScreenLiveData: LiveData> - get() = _finishScreenLiveData - private val _gotoBiometricsSetupLiveData = MutableLiveData>() - val gotoBiometricsSetupLiveData: LiveData> - get() = _gotoBiometricsSetupLiveData - - fun initialize() { - } - - fun hasFinishedSetupPassword() { - session.hasFinishedSetupPassword() - } - - fun startSetupPassword(password: String) { - // Keep password for re-enter check and biometrics activation - session.startPasswordSetup(password) - } - - fun checkPasswordRequirements(password: String): Boolean { - return (password.length == 6) - } - - fun setupPassword(password: String, continueFlow: Boolean) { - val res = App.appCore.getCurrentAuthenticationManager().createPasswordCheck(password) - if (res) { - // Setting up password is done, so login screen should be shown next time app is opened - session.hasSetupPassword(true) - if (BiometricsUtil.isBiometricsAvailable()) { - _gotoBiometricsSetupLiveData.value = Event(true) - } else { - session.hasFinishedSetupPassword() - if (continueFlow) { // if we continue flow (setting up account and identity) we are at the end of the line and we clear stored password - session.hasFinishedSetupPassword() - } - _finishScreenLiveData.value = Event(true) - } - } else { - _errorLiveData.value = Event(true) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/CcxPasscodeInputView.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/CcxPasscodeInputView.kt similarity index 98% rename from app/src/main/java/com/concordium/wallet/ui/auth/passcode/CcxPasscodeInputView.kt rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/CcxPasscodeInputView.kt index bc97afc8..9307a2a0 100644 --- a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/CcxPasscodeInputView.kt +++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/CcxPasscodeInputView.kt @@ -1,4 +1,4 @@ -package com.concordium.wallet.ui.auth.passcode +package com.concordium.wallet.ui.auth.setup import android.content.Context import android.util.AttributeSet diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordActivity.kt similarity index 86% rename from app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordActivity.kt rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordActivity.kt index 04682e54..d54edf9f 100644 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordActivity.kt +++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordActivity.kt @@ -1,4 +1,4 @@ -package com.concordium.wallet.ui.auth.setuppassword +package com.concordium.wallet.ui.auth.setup.password import android.app.Activity import android.content.Intent @@ -9,12 +9,14 @@ import androidx.lifecycle.ViewModelProvider import com.concordium.wallet.R import com.concordium.wallet.core.arch.EventObserver import com.concordium.wallet.databinding.ActivityAuthSetupPasswordBinding -import com.concordium.wallet.ui.auth.setupbiometrics.AuthSetupBiometricsActivity -import com.concordium.wallet.ui.auth.setuppasswordrepeat.AuthSetupPasswordRepeatActivity import com.concordium.wallet.ui.base.BaseActivity import com.concordium.wallet.uicore.afterTextChanged import com.concordium.wallet.util.KeyboardUtil +/** + * This screen should only be called from the passcode setup. + * It does not suggest biometrics and does not finalize session password setup. + */ class AuthSetupPasswordActivity : BaseActivity( R.layout.activity_auth_setup_password, R.string.auth_setup_password_title @@ -24,14 +26,9 @@ class AuthSetupPasswordActivity : BaseActivity( ActivityAuthSetupPasswordBinding.bind(findViewById(R.id.root_layout)) } - private val skipBiometrics: Boolean by lazy { - intent.getBooleanExtra(SKIP_BIOMETRICS, false) - } - companion object { private const val REQUEST_CODE_AUTH_SETUP_BIOMETRICS = 2000 private const val REQUEST_CODE_AUTH_SETUP_PASSWORD_REPEAT = 2001 - const val SKIP_BIOMETRICS = "skip_biometrics" } //region Lifecycle @@ -41,7 +38,7 @@ class AuthSetupPasswordActivity : BaseActivity( super.onCreate(savedInstanceState) initializeViewModel() - viewModel.initialize(skipBiometrics) + viewModel.initialize() initializeViews() hideActionBarBack(isVisible = true) @@ -99,13 +96,6 @@ class AuthSetupPasswordActivity : BaseActivity( } } }) - viewModel.gotoBiometricsSetupLiveData.observe(this, object : EventObserver() { - override fun onUnhandledEvent(value: Boolean) { - if (value) { - gotoAuthSetupBiometrics() - } - } - }) } private fun initializeViews() { @@ -157,11 +147,6 @@ class AuthSetupPasswordActivity : BaseActivity( } } - private fun gotoAuthSetupBiometrics() { - val intent = Intent(this, AuthSetupBiometricsActivity::class.java) - startActivityForResult(intent, REQUEST_CODE_AUTH_SETUP_BIOMETRICS) - } - private fun gotoAuthSetupPasswordRepeat() { val intent = Intent(this, AuthSetupPasswordRepeatActivity::class.java) startActivityForResult(intent, REQUEST_CODE_AUTH_SETUP_PASSWORD_REPEAT) diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatActivity.kt similarity index 98% rename from app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatActivity.kt rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatActivity.kt index 4d3a7f61..c50fc642 100644 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatActivity.kt +++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatActivity.kt @@ -1,4 +1,4 @@ -package com.concordium.wallet.ui.auth.setuppasswordrepeat +package com.concordium.wallet.ui.auth.setup.password import android.app.Activity import android.content.Intent diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatViewModel.kt similarity index 69% rename from app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatViewModel.kt rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatViewModel.kt index 3dd69a59..c807bd08 100644 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatViewModel.kt @@ -1,4 +1,4 @@ -package com.concordium.wallet.ui.auth.setuppasswordrepeat +package com.concordium.wallet.ui.auth.setup.password import android.app.Application import androidx.lifecycle.AndroidViewModel @@ -6,13 +6,9 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.concordium.wallet.App import com.concordium.wallet.core.arch.Event -import com.concordium.wallet.core.authentication.Session class AuthSetupPasswordRepeatViewModel(application: Application) : AndroidViewModel(application) { - // App.appCore.getCurrentAuthenticationManager() - private val session: Session = App.appCore.session - private val _finishScreenLiveData = MutableLiveData>() val finishScreenLiveData: LiveData> get() = _finishScreenLiveData @@ -21,7 +17,7 @@ class AuthSetupPasswordRepeatViewModel(application: Application) : AndroidViewMo } fun checkPassword(password: String) { - val isEqual = session.checkPassword(password) + val isEqual = App.appCore.setup.authSetupPassword == password _finishScreenLiveData.value = Event(isEqual) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordViewModel.kt new file mode 100644 index 00000000..060a25fb --- /dev/null +++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordViewModel.kt @@ -0,0 +1,56 @@ +package com.concordium.wallet.ui.auth.setup.password + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.concordium.wallet.App +import com.concordium.wallet.core.arch.Event +import kotlinx.coroutines.launch + +class AuthSetupPasswordViewModel(application: Application) : AndroidViewModel(application) { + + private val _errorLiveData = MutableLiveData>() + val errorLiveData: LiveData> + get() = _errorLiveData + private val _finishScreenLiveData = MutableLiveData>() + val finishScreenLiveData: LiveData> + get() = _finishScreenLiveData + + fun initialize() { + } + + fun startSetupPassword(password: String) { + // Keep password for re-enter check and biometrics activation (outside this screen). + App.appCore.setup.beginAuthSetup(password) + } + + fun checkPasswordRequirements(password: String): Boolean { + return (password.length >= 6) + } + + fun setupPassword(password: String) = viewModelScope.launch { + val isSetUpSuccessfully = runCatching { + val authResetMasterKey = App.appCore.setup.authResetMasterKey + if (authResetMasterKey != null) { + App.appCore.auth.initPasswordAuth( + password = password, + isPasscode = false, + masterKey = authResetMasterKey, + ) + } else { + App.appCore.auth.initPasswordAuth( + password = password, + isPasscode = false, + ) + } + }.isSuccess + + if (isSetUpSuccessfully) { + _finishScreenLiveData.value = Event(true) + } else { + _errorLiveData.value = Event(true) + } + } +} diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsActivity.kt deleted file mode 100644 index fb1e1585..00000000 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsActivity.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.concordium.wallet.ui.auth.setupbiometrics - -import android.app.Activity -import android.os.Bundle -import androidx.biometric.BiometricPrompt -import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider -import com.concordium.wallet.R -import com.concordium.wallet.core.arch.EventObserver -import com.concordium.wallet.core.security.BiometricPromptCallback -import com.concordium.wallet.databinding.ActivityAuthSetupBiometricsBinding -import com.concordium.wallet.ui.base.BaseActivity -import com.concordium.wallet.ui.common.delegates.AuthDelegate -import com.concordium.wallet.ui.common.delegates.AuthDelegateImpl -import javax.crypto.Cipher - -class AuthSetupBiometricsActivity : BaseActivity( - R.layout.activity_auth_setup_biometrics, - R.string.auth_setup_biometrics_title -), AuthDelegate by AuthDelegateImpl() { - private lateinit var viewModel: AuthSetupBiometricsViewModel - private val binding by lazy { - ActivityAuthSetupBiometricsBinding.bind(findViewById(R.id.root_layout)) - } - private lateinit var biometricPrompt: BiometricPrompt - - //region Lifecycle - // ************************************************************ - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - initializeViewModel() - viewModel.initialize() - initializeViews() - - hideActionBarBack(isVisible = false) - - biometricPrompt = createBiometricPrompt() - } - - override fun onBackPressed() { - // Ignore back press - } - - //endregion - - //region Initialize - // ************************************************************ - - private fun initializeViewModel() { - viewModel = ViewModelProvider( - this, - ViewModelProvider.AndroidViewModelFactory.getInstance(application) - )[AuthSetupBiometricsViewModel::class.java] - - viewModel.errorLiveData.observe(this, object : EventObserver() { - override fun onUnhandledEvent(value: Int) { - showError(value) - } - }) - viewModel.finishScreenLiveData.observe(this, object : EventObserver() { - override fun onUnhandledEvent(value: Boolean) { - if (value) { - setResult(Activity.RESULT_OK) - finish() - } - } - }) - } - - private fun initializeViews() { - hideActionBarBack(false) - binding.enableBiometricsButton.setOnClickListener { - onEnableBiometricsClicked() - } - binding.cancelButton.setOnClickListener { - onCancelClicked() - } - } - - //endregion - - //region Control/UI - // ************************************************************ - - private fun onEnableBiometricsClicked() { - val promptInfo = createPromptInfo() - - val cipher = viewModel.getCipherForBiometrics() - if (cipher != null) { - biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) - } - } - - private fun onCancelClicked() { - setResult(Activity.RESULT_OK) - finish() - } - - //endregion - - //region Biometrics - // ************************************************************ - - private fun createBiometricPrompt(): BiometricPrompt { - val executor = ContextCompat.getMainExecutor(this) - - val callback = object : BiometricPromptCallback() { - override fun onAuthenticationSucceeded(cipher: Cipher) { - viewModel.setupBiometricWithPassword(cipher) - } - } - - return BiometricPrompt(this, executor, callback) - } - - private fun createPromptInfo(): BiometricPrompt.PromptInfo { - return BiometricPrompt.PromptInfo.Builder() - .setTitle(getString(R.string.auth_setup_biometrics_dialog_title)) - .setSubtitle(getString(R.string.auth_setup_biometrics_dialog_subtitle)) - .setConfirmationRequired(false) - .setNegativeButtonText(getString(R.string.auth_setup_biometrics_dialog_cancel)) - .build() - } - - //endregion - - - override fun loggedOut() { - // No need to show auth, as it is anyway requested further. - } -} diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordViewModel.kt deleted file mode 100644 index 6ccaa21c..00000000 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordViewModel.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.concordium.wallet.ui.auth.setuppassword - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.concordium.wallet.App -import com.concordium.wallet.core.arch.Event -import com.concordium.wallet.core.authentication.Session -import com.concordium.wallet.util.BiometricsUtil - -class AuthSetupPasswordViewModel(application: Application) : AndroidViewModel(application) { - - private val session: Session = App.appCore.session - - private val _errorLiveData = MutableLiveData>() - val errorLiveData: LiveData> - get() = _errorLiveData - private val _finishScreenLiveData = MutableLiveData>() - val finishScreenLiveData: LiveData> - get() = _finishScreenLiveData - private val _gotoBiometricsSetupLiveData = MutableLiveData>() - val gotoBiometricsSetupLiveData: LiveData> - get() = _gotoBiometricsSetupLiveData - private var skipBiometrics = true - - fun initialize(skipBiometrics: Boolean) { - this.skipBiometrics = skipBiometrics - } - - fun startSetupPassword(password: String) { - // Keep password for re-enter check and biometrics activation - session.startPasswordSetup(password) - } - - fun checkPasswordRequirements(password: String): Boolean { - return (password.length >= 6) - } - - fun setupPassword(password: String) { - val res = App.appCore.getCurrentAuthenticationManager().createPasswordCheck(password) - if (res) { - // Setting up password is done, so login screen should be shown next time app is opened - session.hasSetupPassword() - if (BiometricsUtil.isBiometricsAvailable()) { - if (skipBiometrics) { - _finishScreenLiveData.value = Event(true) - } else { - _gotoBiometricsSetupLiveData.value = Event(true) - } - } else { - session.hasFinishedSetupPassword() - _finishScreenLiveData.value = Event(true) - } - } else { - _errorLiveData.value = Event(true) - } - } -} diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatActivity.kt deleted file mode 100644 index 5a8b96ca..00000000 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatActivity.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.concordium.wallet.ui.auth.setuprepeat - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import androidx.lifecycle.ViewModelProvider -import com.concordium.wallet.R -import com.concordium.wallet.core.arch.EventObserver -import com.concordium.wallet.databinding.ActivityAuthSetupBinding -import com.concordium.wallet.ui.base.BaseActivity -import com.concordium.wallet.uicore.view.PasscodeView - -class AuthSetupRepeatActivity : BaseActivity( - R.layout.activity_auth_setup, - R.string.auth_setup_repeat_title -) { - private lateinit var viewModel: AuthSetupRepeatViewModel - private val binding by lazy { - ActivityAuthSetupBinding.bind(findViewById(R.id.root_layout)) - } - - //region Lifecycle - // ************************************************************ - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - initializeViewModel() - viewModel.initialize() - initializeViews() - - hideActionBarBack(isVisible = false) - } - - //endregion - - //region Initialize - // ************************************************************ - - private fun initializeViewModel() { - viewModel = ViewModelProvider( - this, - ViewModelProvider.AndroidViewModelFactory.getInstance(application) - )[AuthSetupRepeatViewModel::class.java] - - viewModel.finishScreenLiveData.observe(this, object : EventObserver() { - override fun onUnhandledEvent(value: Boolean) { - setResult( - Activity.RESULT_OK, - createResult( - doesMatch = value, - useFullPassword = false - ) - ) - finish() - } - }) - } - - private fun initializeViews() { - binding.instructionTextview.setText(R.string.auth_setup_repeat_info) - binding.passcodeView.passcodeListener = object : PasscodeView.PasscodeListener { - override fun onInputChanged() { - } - - override fun onDone() { - onConfirmClicked() - } - } - binding.fullPasswordButton.setOnClickListener { - setResult( - Activity.RESULT_OK, - createResult( - doesMatch = false, - useFullPassword = true - ) - ) - finish() - } - binding.passcodeView.requestFocus() - } - - //endregion - - //region Control/UI - // ************************************************************ - - private fun onConfirmClicked() { - viewModel.checkPassword(binding.passcodeView.getPasscode()) - } - - //endregion - - override fun loggedOut() { - // No need to show auth, as it is anyway requested further. - } - - companion object { - private const val DOES_MATCH_EXTRA = "does_match" - private const val USE_FULL_PASSWORD_EXTRA = "use_full_password" - - private fun createResult( - doesMatch: Boolean, - useFullPassword: Boolean, - ) = Intent().apply { - putExtra(DOES_MATCH_EXTRA, doesMatch) - putExtra(USE_FULL_PASSWORD_EXTRA, useFullPassword) - } - - fun doesMatch(result: Intent): Boolean = - result.getBooleanExtra(DOES_MATCH_EXTRA, false) - - fun useFullPassword(result: Intent): Boolean = - result.getBooleanExtra(USE_FULL_PASSWORD_EXTRA, false) - } -} diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatViewModel.kt deleted file mode 100644 index 57f5aa0d..00000000 --- a/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatViewModel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.concordium.wallet.ui.auth.setuprepeat - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.concordium.wallet.App -import com.concordium.wallet.core.arch.Event -import com.concordium.wallet.core.authentication.Session - -class AuthSetupRepeatViewModel(application: Application) : AndroidViewModel(application) { - -// App.appCore.getCurrentAuthenticationManager() - private val session: Session = App.appCore.session - - private val _finishScreenLiveData = MutableLiveData>() - val finishScreenLiveData: LiveData> - get() = _finishScreenLiveData - - fun initialize() { - } - - fun checkPassword(password: String) { - val isEqual = session.checkPassword(password) - _finishScreenLiveData.value = Event(isEqual) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/concordium/wallet/ui/bakerdelegation/common/DelegationBakerViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/bakerdelegation/common/DelegationBakerViewModel.kt index b80f8bdf..e6c1d141 100644 --- a/app/src/main/java/com/concordium/wallet/ui/bakerdelegation/common/DelegationBakerViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/bakerdelegation/common/DelegationBakerViewModel.kt @@ -2,7 +2,6 @@ package com.concordium.wallet.ui.bakerdelegation.common import android.app.Application import android.net.Uri -import android.text.TextUtils import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -41,7 +40,6 @@ import com.concordium.wallet.data.model.TransactionStatus import com.concordium.wallet.data.model.TransactionType import com.concordium.wallet.data.model.TransferSubmissionStatus import com.concordium.wallet.data.room.Transfer -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.data.util.FileUtil import com.concordium.wallet.ui.common.BackendErrorHandler import com.concordium.wallet.util.DateTimeUtil @@ -58,7 +56,8 @@ class DelegationBakerViewModel(application: Application) : AndroidViewModel(appl lateinit var bakerDelegationData: BakerDelegationData private val proxyRepository = ProxyRepository() - private val transferRepository: TransferRepository + private val transferRepository = + TransferRepository(App.appCore.session.walletStorage.database.transferDao()) private var bakerPoolRequest: BackendRequest? = null private var accountNonceRequest: BackendRequest? = null @@ -116,11 +115,6 @@ class DelegationBakerViewModel(application: Application) : AndroidViewModel(appl val bakerPoolStatusLiveData: MutableLiveData get() = _bakerPoolStatusLiveData - init { - val transferDao = WalletDatabase.getDatabase(application).transferDao() - transferRepository = TransferRepository(transferDao) - } - fun initialize(bakerDelegationData: BakerDelegationData) { this.bakerDelegationData = bakerDelegationData } @@ -462,13 +456,17 @@ class DelegationBakerViewModel(application: Application) : AndroidViewModel(appl Log.d("decryptAndContinue") bakerDelegationData.account?.let { account -> val storageAccountDataEncrypted = account.encryptedAccountData - if (TextUtils.isEmpty(storageAccountDataEncrypted)) { + if (storageAccountDataEncrypted == null) { _errorLiveData.value = Event(R.string.app_error_general) _waitingLiveData.value = false return } - val decryptedJson = App.appCore.getCurrentAuthenticationManager() - .decryptInBackground(password, storageAccountDataEncrypted) + val decryptedJson = App.appCore.auth + .decrypt( + password = password, + encryptedData = storageAccountDataEncrypted, + ) + ?.let(::String) if (decryptedJson != null) { val credentialsOutput = @@ -759,7 +757,7 @@ class DelegationBakerViewModel(application: Application) : AndroidViewModel(appl } } - fun bakerKeysJson(): String? { + private fun bakerKeysJson(): String? { _bakerKeysLiveData.value?.let { bakerKeys -> bakerKeys.bakerId = bakerDelegationData.account?.index return if (bakerKeys.toString() diff --git a/app/src/main/java/com/concordium/wallet/ui/base/BaseActivity.kt b/app/src/main/java/com/concordium/wallet/ui/base/BaseActivity.kt index 1e766d92..8c620f75 100644 --- a/app/src/main/java/com/concordium/wallet/ui/base/BaseActivity.kt +++ b/app/src/main/java/com/concordium/wallet/ui/base/BaseActivity.kt @@ -9,6 +9,7 @@ import android.os.storage.StorageManager import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.activity.result.ActivityResultLauncher @@ -19,7 +20,6 @@ import androidx.appcompat.widget.Toolbar import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.core.view.isVisible -import androidx.lifecycle.Observer import com.concordium.wallet.App import com.concordium.wallet.Constants import com.concordium.wallet.Constants.Extras.EXTRA_ADD_CONTACT @@ -52,9 +52,12 @@ abstract class BaseActivity( var isActive = false private var backBtn: ImageView? = null - private var plusLeftBtn: ImageView? = null - private var plusRightBtn: ImageView? = null + private var plusLeftBtn: FrameLayout? = null + private var plusLeftBtnNotice: View? = null + private var plusRightBtn: FrameLayout? = null + private var plusRightBtnNotice: View? = null private var qrScanBtn: ImageView? = null + private var infoBtn: ImageView? = null protected var closeBtn: ImageView? = null protected var deleteBtn: ImageView? = null private var settingsBtn: ImageView? = null @@ -81,8 +84,11 @@ abstract class BaseActivity( backBtn = toolbar?.findViewById(R.id.toolbar_back_btn) plusLeftBtn = toolbar?.findViewById(R.id.toolbar_plus_btn) + plusLeftBtnNotice = toolbar?.findViewById(R.id.toolbar_plus_btn_notice) plusRightBtn = toolbar?.findViewById(R.id.toolbar_plus_btn_add_contact) + plusRightBtnNotice = toolbar?.findViewById(R.id.toolbar_plus_btn_add_contact_notice) qrScanBtn = toolbar?.findViewById(R.id.toolbar_qr_btn) + infoBtn = toolbar?.findViewById(R.id.toolbar_info_btn) closeBtn = toolbar?.findViewById(R.id.toolbar_close_btn) deleteBtn = toolbar?.findViewById(R.id.toolbar_delete_btn) settingsBtn = toolbar?.findViewById(R.id.toolbar_settings_btn) @@ -96,15 +102,15 @@ abstract class BaseActivity( popup = Popup() dialogs = Dialogs() - App.appCore.session.isLoggedIn.observe(this, Observer { loggedin -> - if (App.appCore.session.hasSetupPassword) { - if (loggedin) { + App.appCore.session.isLoggedIn.observe(this) { loggedIn -> + if (App.appCore.setup.isAuthSetupCompleted) { + if (loggedIn) { loggedIn() } else { loggedOut() } } - }) + } } override fun onResume() { @@ -202,14 +208,29 @@ abstract class BaseActivity( } } - fun hideRightPlus(isVisible: Boolean, listener: View.OnClickListener? = null) { + fun hideInfo(isVisible: Boolean, listener: View.OnClickListener? = null) { + infoBtn?.isVisible = isVisible + infoBtn?.setOnClickListener(listener) + } + + fun hideRightPlus( + isVisible: Boolean, + hasNotice: Boolean = false, + listener: View.OnClickListener? = null + ) { plusRightBtn?.isVisible = isVisible plusRightBtn?.setOnClickListener(listener) + plusRightBtnNotice?.isVisible = hasNotice } - fun hideLeftPlus(isVisible: Boolean, listener: View.OnClickListener? = null) { + fun hideLeftPlus( + isVisible: Boolean, + hasNotice: Boolean = false, + listener: View.OnClickListener? = null + ) { plusLeftBtn?.isVisible = isVisible plusLeftBtn?.setOnClickListener(listener) + plusLeftBtnNotice?.isVisible = hasNotice } fun hideSettings(isVisible: Boolean, listener: View.OnClickListener? = null) { @@ -271,8 +292,8 @@ abstract class BaseActivity( } fun showAuthentication(text: String = authenticateText(), callback: AuthenticationCallback) { - val useBiometrics = App.appCore.getCurrentAuthenticationManager().useBiometrics() - val usePasscode = App.appCore.getCurrentAuthenticationManager().usePasscode() + val useBiometrics = App.appCore.auth.isBiometricsUsed() + val usePasscode = App.appCore.auth.isPasscodeUsed() if (useBiometrics) { showBiometrics(text, usePasscode, callback) } else { @@ -314,7 +335,10 @@ abstract class BaseActivity( } } - private fun createBiometricPrompt(text: String?, callback: AuthenticationCallback): BiometricPrompt { + private fun createBiometricPrompt( + text: String?, + callback: AuthenticationCallback + ): BiometricPrompt { val executor = ContextCompat.getMainExecutor(this) val callback = object : BiometricPromptCallback() { @@ -345,8 +369,8 @@ abstract class BaseActivity( } fun authenticateText(): String { - val useBiometrics = App.appCore.getCurrentAuthenticationManager().useBiometrics() - val usePasscode = App.appCore.getCurrentAuthenticationManager().usePasscode() + val useBiometrics = App.appCore.auth.isBiometricsUsed() + val usePasscode = App.appCore.auth.isPasscodeUsed() return when { useBiometrics -> getString(R.string.auth_login_biometrics_dialog_subtitle) usePasscode -> getString(R.string.auth_login_biometrics_dialog_cancel_passcode) diff --git a/app/src/main/java/com/concordium/wallet/ui/cis2/SendTokenViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/cis2/SendTokenViewModel.kt index 7673f39f..b815d96f 100644 --- a/app/src/main/java/com/concordium/wallet/ui/cis2/SendTokenViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/cis2/SendTokenViewModel.kt @@ -1,7 +1,6 @@ package com.concordium.wallet.ui.cis2 import android.app.Application -import android.text.TextUtils import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope @@ -13,6 +12,7 @@ import com.concordium.wallet.data.AccountRepository import com.concordium.wallet.data.ContractTokensRepository import com.concordium.wallet.data.TransferRepository import com.concordium.wallet.data.backend.repository.ProxyRepository +import com.concordium.wallet.data.cryptolib.ContractAddress import com.concordium.wallet.data.cryptolib.CreateAccountTransactionInput import com.concordium.wallet.data.cryptolib.CreateTransferInput import com.concordium.wallet.data.cryptolib.CreateTransferOutput @@ -29,14 +29,12 @@ import com.concordium.wallet.data.model.Token import com.concordium.wallet.data.model.TransactionOutcome import com.concordium.wallet.data.model.TransactionStatus import com.concordium.wallet.data.model.TransactionType +import com.concordium.wallet.data.preferences.WalletSendFundsPreferences import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.room.Transfer -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.data.walletconnect.AccountTransactionPayload -import com.concordium.wallet.data.cryptolib.ContractAddress import com.concordium.wallet.ui.account.common.accountupdater.AccountUpdater import com.concordium.wallet.ui.common.BackendErrorHandler -import com.concordium.wallet.ui.transaction.sendfunds.SendFundsPreferences import com.concordium.wallet.ui.transaction.sendfunds.SendFundsViewModel import com.concordium.wallet.util.CBORUtil import com.concordium.wallet.util.DateTimeUtil @@ -78,11 +76,11 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio } private val proxyRepository = ProxyRepository() - private val transferRepository: TransferRepository + private val transferRepository = + TransferRepository(App.appCore.session.walletStorage.database.transferDao()) private val accountUpdater = AccountUpdater(application, viewModelScope) - private val sendFundsPreferences by lazy { - SendFundsPreferences(getApplication()) - } + private val sendFundsPreferences: WalletSendFundsPreferences = + App.appCore.session.walletStorage.sendFundsPreferences private var accountNonceRequest: BackendRequest? = null private var globalParamsRequest: BackendRequest? = null @@ -108,9 +106,6 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio } init { - transferRepository = - TransferRepository(WalletDatabase.getDatabase(application).transferDao()) - chooseToken.observeForever { token -> sendTokenData.token = token sendTokenData.max = if (token.isCcd) null else token.balance @@ -131,7 +126,7 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio waiting.postValue(true) CoroutineScope(Dispatchers.IO).launch { val contractTokensRepository = ContractTokensRepository( - WalletDatabase.getDatabase(getApplication()).contractTokenDao() + App.appCore.session.walletStorage.database.contractTokenDao() ) val tokensFound = mutableListOf() tokensFound.add(getCCDDefaultToken(accountAddress)) @@ -201,11 +196,11 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio sendTokenData.memo?.let(CBORUtil.Companion::decodeHexAndCBOR) fun showMemoWarning(): Boolean { - return sendFundsPreferences.showMemoWarning() + return sendFundsPreferences.shouldShowMemoWarning() } fun dontShowMemoWarning() { - return sendFundsPreferences.dontShowMemoWarning() + return sendFundsPreferences.disableShowMemoWarning() } fun onReceiverEntered(input: String) { @@ -331,7 +326,7 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio private suspend fun getCCDDefaultToken(accountAddress: String): Token { val accountRepository = - AccountRepository(WalletDatabase.getDatabase(getApplication()).accountDao()) + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) val account = accountRepository.findByAddress(accountAddress) ?: error("Account $accountAddress not found") return Token.ccd(account) @@ -345,16 +340,21 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio private suspend fun decryptAndContinue(password: String) { sendTokenData.account?.let { account -> val storageAccountDataEncrypted = account.encryptedAccountData - if (TextUtils.isEmpty(storageAccountDataEncrypted)) { + if (storageAccountDataEncrypted == null) { errorInt.postValue(R.string.app_error_general) waiting.postValue(false) return } - val decryptedJson = App.appCore.getCurrentAuthenticationManager() - .decryptInBackground(password, storageAccountDataEncrypted) - val credentialsOutput = - App.appCore.gson.fromJson(decryptedJson, StorageAccountData::class.java) + val decryptedJson = App.appCore.auth + .decrypt( + password = password, + encryptedData = storageAccountDataEncrypted + ) + ?.let(::String) + if (decryptedJson != null) { + val credentialsOutput = + App.appCore.gson.fromJson(decryptedJson, StorageAccountData::class.java) getAccountEncryptedKey(credentialsOutput) } else { errorInt.postValue(R.string.app_error_encryption) diff --git a/app/src/main/java/com/concordium/wallet/ui/cis2/TokensViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/cis2/TokensViewModel.kt index d105274c..e5965183 100644 --- a/app/src/main/java/com/concordium/wallet/ui/cis2/TokensViewModel.kt +++ b/app/src/main/java/com/concordium/wallet/ui/cis2/TokensViewModel.kt @@ -4,13 +4,13 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.concordium.wallet.App import com.concordium.wallet.data.AccountRepository import com.concordium.wallet.data.ContractTokensRepository import com.concordium.wallet.data.backend.repository.ProxyRepository import com.concordium.wallet.data.model.Token import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.room.ContractToken -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.ui.cis2.retrofit.IncorrectChecksumException import com.concordium.wallet.ui.cis2.retrofit.MetadataApiInstance import com.concordium.wallet.ui.common.BackendErrorHandler @@ -66,7 +66,7 @@ class TokensViewModel(application: Application) : AndroidViewModel(application) private val proxyRepository = ProxyRepository() private val contractTokensRepository: ContractTokensRepository by lazy { ContractTokensRepository( - WalletDatabase.getDatabase(getApplication()).contractTokenDao() + App.appCore.session.walletStorage.database.contractTokenDao() ) } @@ -445,7 +445,7 @@ class TokensViewModel(application: Application) : AndroidViewModel(application) private suspend fun getCCDDefaultToken(accountAddress: String): Token { val accountRepository = - AccountRepository(WalletDatabase.getDatabase(getApplication()).accountDao()) + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) val account = accountRepository.findByAddress(accountAddress) ?: error("Account $accountAddress not found") return Token.ccd(account) diff --git a/app/src/main/java/com/concordium/wallet/ui/common/delegates/AuthDelegate.kt b/app/src/main/java/com/concordium/wallet/ui/common/delegates/AuthDelegate.kt index 6c50e64b..cbdf2fff 100644 --- a/app/src/main/java/com/concordium/wallet/ui/common/delegates/AuthDelegate.kt +++ b/app/src/main/java/com/concordium/wallet/ui/common/delegates/AuthDelegate.kt @@ -27,16 +27,16 @@ class AuthDelegateImpl : AuthDelegate { ) { activity.showAuthentication(callback = object : BaseActivity.AuthenticationCallback { override fun getCipherForBiometrics(): Cipher? = - App.appCore.getCurrentAuthenticationManager() - .initBiometricsCipherForDecryption() + App.appCore.auth + .getBiometricsCipherForDecryption() override fun onCorrectPassword(password: String) = onAuthenticated(password) override fun onCipher(cipher: Cipher) { activity.lifecycleScope.launch(Dispatchers.IO) { - val password = App.appCore.getCurrentAuthenticationManager() - .checkPasswordInBackground(cipher) + val password = App.appCore.auth + .decryptPasswordWithBiometricsCipher(cipher) if (password != null) { withContext(Dispatchers.Main) { onAuthenticated(password) diff --git a/app/src/main/java/com/concordium/wallet/ui/common/delegates/IdentityStatusDelegate.kt b/app/src/main/java/com/concordium/wallet/ui/common/delegates/IdentityStatusDelegate.kt index ff528caf..647d6307 100644 --- a/app/src/main/java/com/concordium/wallet/ui/common/delegates/IdentityStatusDelegate.kt +++ b/app/src/main/java/com/concordium/wallet/ui/common/delegates/IdentityStatusDelegate.kt @@ -1,13 +1,12 @@ package com.concordium.wallet.ui.common.delegates +import android.app.Activity import android.content.Intent -import androidx.core.app.ComponentActivity import com.concordium.wallet.App import com.concordium.wallet.R import com.concordium.wallet.data.IdentityRepository import com.concordium.wallet.data.model.IdentityStatus import com.concordium.wallet.data.room.Identity -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.ui.MainViewModel import com.concordium.wallet.ui.base.BaseActivity import com.concordium.wallet.ui.identity.identityconfirmed.IdentityConfirmedActivity @@ -24,20 +23,20 @@ import kotlin.concurrent.schedule interface IdentityStatusDelegate { fun startCheckForPendingIdentity( - activity: ComponentActivity?, + activity: Activity?, specificIdentityId: Int?, showForFirstIdentity: Boolean, statusChanged: (Identity) -> Unit ) fun identityDone( - activity: ComponentActivity, + activity: Activity, identity: Identity, statusChanged: (Identity) -> Unit ) fun identityError( - activity: ComponentActivity, + activity: Activity, identity: Identity, statusChanged: (Identity) -> Unit ) @@ -50,7 +49,7 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate { private var showForFirstIdentity = false override fun startCheckForPendingIdentity( - activity: ComponentActivity?, + activity: Activity?, specificIdentityId: Int?, showForFirstIdentity: Boolean, statusChanged: (Identity) -> Unit @@ -58,13 +57,13 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate { this.showForFirstIdentity = showForFirstIdentity if (activity == null || activity.isFinishing || activity.isDestroyed) return - if (App.appCore.newIdentities.isNotEmpty()) { - for (newIdentity in App.appCore.newIdentities) { + if (App.appCore.session.newIdentities.isNotEmpty()) { + for (newIdentity in App.appCore.session.newIdentities) { if (specificIdentityId == null || specificIdentityId == newIdentity.key) { CoroutineScope(Dispatchers.IO).launch { job = launch { val identityRepository = IdentityRepository( - WalletDatabase.getDatabase(activity).identityDao() + App.appCore.session.walletStorage.database.identityDao() ) val identity = identityRepository.findById(newIdentity.key) identity?.let { @@ -114,13 +113,13 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate { } override fun identityDone( - activity: ComponentActivity, + activity: Activity, identity: Identity, statusChanged: (Identity) -> Unit ) { - if (App.appCore.newIdentities[identity.id] == null) + if (App.appCore.session.newIdentities[identity.id] == null) return - App.appCore.newIdentities.remove(identity.id) + App.appCore.session.newIdentities.remove(identity.id) if (showForFirstIdentity) { statusChanged(identity) @@ -153,13 +152,13 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate { } override fun identityError( - activity: ComponentActivity, + activity: Activity, identity: Identity, statusChanged: (Identity) -> Unit ) { - if (App.appCore.newIdentities[identity.id] == null) + if (App.appCore.session.newIdentities[identity.id] == null) return - App.appCore.newIdentities.remove(identity.id) + App.appCore.session.newIdentities.remove(identity.id) if (showForFirstIdentity) { statusChanged(identity) @@ -176,7 +175,7 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate { } private fun identityErrorNextIdentity( - activity: ComponentActivity, + activity: Activity, identity: Identity, builder: MaterialAlertDialogBuilder, statusChanged: (Identity) -> Unit diff --git a/app/src/main/java/com/concordium/wallet/ui/common/identity/IdentityUpdater.kt b/app/src/main/java/com/concordium/wallet/ui/common/identity/IdentityUpdater.kt index 18fea960..3edcedb6 100644 --- a/app/src/main/java/com/concordium/wallet/ui/common/identity/IdentityUpdater.kt +++ b/app/src/main/java/com/concordium/wallet/ui/common/identity/IdentityUpdater.kt @@ -14,7 +14,6 @@ import com.concordium.wallet.data.model.TransactionStatus import com.concordium.wallet.data.room.Account import com.concordium.wallet.data.room.Identity import com.concordium.wallet.data.room.Recipient -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.ui.cis2.defaults.DefaultFungibleTokensManager import com.concordium.wallet.ui.cis2.defaults.DefaultTokensManagerFactory import com.concordium.wallet.util.Log @@ -30,28 +29,24 @@ import java.net.URL class IdentityUpdater(val application: Application, private val viewModelScope: CoroutineScope) { private val gson = App.appCore.gson - private val identityRepository: IdentityRepository - private val accountRepository: AccountRepository - private val recipientRepository: RecipientRepository + private val identityRepository = + IdentityRepository(App.appCore.session.walletStorage.database.identityDao()) + private val accountRepository = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) + private val recipientRepository = + RecipientRepository(App.appCore.session.walletStorage.database.recipientDao()) private val defaultFungibleTokensManager: DefaultFungibleTokensManager - private val updateNotificationsSubscriptionUseCase by lazy { - UpdateNotificationsSubscriptionUseCase(application) - } + private val updateNotificationsSubscriptionUseCase by lazy(::UpdateNotificationsSubscriptionUseCase) private var updateListener: UpdateListener? = null private var run = true init { - val identityDao = WalletDatabase.getDatabase(application).identityDao() - identityRepository = IdentityRepository(identityDao) - val accountDao = WalletDatabase.getDatabase(application).accountDao() - accountRepository = AccountRepository(accountDao) - val recipientDao = WalletDatabase.getDatabase(application).recipientDao() - recipientRepository = RecipientRepository(recipientDao) - - val contractTokenDao = WalletDatabase.getDatabase(application).contractTokenDao() + val defaultTokensManagerFactory = DefaultTokensManagerFactory( - contractTokensRepository = ContractTokensRepository(contractTokenDao), + contractTokensRepository = ContractTokensRepository( + App.appCore.session.walletStorage.database.contractTokenDao() + ), ) defaultFungibleTokensManager = defaultTokensManagerFactory.getDefaultFungibleTokensManager() } @@ -102,9 +97,13 @@ class IdentityUpdater(val application: Application, private val viewModelScope: val resp = URL(identity.codeUri).readText() Log.d("Identity poll response: $resp") - val identityTokenContainer = gson.fromJson(resp, IdentityTokenContainer::class.java) + val identityTokenContainer = gson.fromJson( + resp, + IdentityTokenContainer::class.java + ) - val newStatus = if (BuildConfig.FAIL_IDENTITY_CREATION) IdentityStatus.ERROR else identityTokenContainer.status + val newStatus = + if (BuildConfig.FAIL_IDENTITY_CREATION) IdentityStatus.ERROR else identityTokenContainer.status if (newStatus != IdentityStatus.PENDING) { identity.status = identityTokenContainer.status @@ -117,7 +116,8 @@ class IdentityUpdater(val application: Application, private val viewModelScope: val accountAddress = token.accountAddress identity.identityObject = identityContainer.value identityRepository.update(identity) - val account = accountRepository.getAllByIdentityId(identity.id).firstOrNull() + val account = + accountRepository.getAllByIdentityId(identity.id).firstOrNull() account?.let { if (it.address == accountAddress) { // it.credential = CredentialWrapper(RawJson(gson.toJson(CredentialContentWrapper(accountCredentialWrapper.value))), accountCredentialWrapper.v) //Make up for protocol inconsistency @@ -143,14 +143,15 @@ class IdentityUpdater(val application: Application, private val viewModelScope: } else if (newStatus == IdentityStatus.ERROR) { identityRepository.update(identity) - val account = accountRepository.getAllByIdentityId(identity.id).firstOrNull() + val account = + accountRepository.getAllByIdentityId(identity.id).firstOrNull() account?.let { accountRepository.delete(it) } withContext(Dispatchers.Main) { updateListener?.onError(identity, account) } } - if (App.appCore.newIdentities[identity.id] == null) { - App.appCore.newIdentities[identity.id] = identity + if (App.appCore.session.newIdentities[identity.id] == null) { + App.appCore.session.newIdentities[identity.id] = identity } } else { hasMorePending = true diff --git a/app/src/main/java/com/concordium/wallet/ui/connect/ConnectActivity.kt b/app/src/main/java/com/concordium/wallet/ui/connect/ConnectActivity.kt index 62dcf92e..87d2b319 100644 --- a/app/src/main/java/com/concordium/wallet/ui/connect/ConnectActivity.kt +++ b/app/src/main/java/com/concordium/wallet/ui/connect/ConnectActivity.kt @@ -14,13 +14,10 @@ import com.concordium.wallet.Constants import com.concordium.wallet.Constants.Extras.EXTRA_ADD_CONTACT import com.concordium.wallet.Constants.Extras.EXTRA_CONNECT_URL import com.concordium.wallet.R -import com.concordium.wallet.data.AccountRepository import com.concordium.wallet.data.backend.OfflineMockInterceptor import com.concordium.wallet.data.backend.ws.WsTransport import com.concordium.wallet.data.model.WsConnectionInfo import com.concordium.wallet.data.model.WsMessageResponse -import com.concordium.wallet.data.room.Account -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.ui.MainActivity import com.concordium.wallet.ui.base.BaseActivity import com.concordium.wallet.ui.common.BackendErrorHandler @@ -43,10 +40,8 @@ class ConnectActivity : BaseActivity(R.layout.activity_connect) { private lateinit var shopDescription: TextView private lateinit var shopLogo: ImageView private var wsTransport: WsTransport? = null - private var accountRepository: AccountRepository? = null private var currentSiteInfo: WsConnectionInfo.SiteInfo? = null private var accountsPool: LinearLayout? = null - private var selectedAccount: Account? = null private val gson by lazy { Gson() @@ -124,9 +119,6 @@ class ConnectActivity : BaseActivity(R.layout.activity_connect) { connectWc(connectUrl) } - val accountDao = WalletDatabase.getDatabase(application).accountDao() - accountRepository = AccountRepository(accountDao) - closeBtn?.visibility = View.VISIBLE closeBtn?.setOnClickListener { finish() diff --git a/app/src/main/java/com/concordium/wallet/ui/connect/add_wallet/AddWalletNftActivity.kt b/app/src/main/java/com/concordium/wallet/ui/connect/add_wallet/AddWalletNftActivity.kt index a4e7451c..54df04c2 100644 --- a/app/src/main/java/com/concordium/wallet/ui/connect/add_wallet/AddWalletNftActivity.kt +++ b/app/src/main/java/com/concordium/wallet/ui/connect/add_wallet/AddWalletNftActivity.kt @@ -9,6 +9,7 @@ import android.widget.Toast import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.children import com.bumptech.glide.Glide +import com.concordium.wallet.App import com.concordium.wallet.Constants import com.concordium.wallet.R import com.concordium.wallet.data.AccountRepository @@ -17,7 +18,6 @@ import com.concordium.wallet.data.model.AccountInfo import com.concordium.wallet.data.model.WsConnectionInfo import com.concordium.wallet.data.model.WsMessageResponse import com.concordium.wallet.data.room.Account -import com.concordium.wallet.data.room.WalletDatabase import com.concordium.wallet.data.util.CurrencyUtil import com.concordium.wallet.ui.base.BaseActivity import com.google.gson.Gson @@ -27,7 +27,8 @@ import kotlinx.coroutines.launch class AddWalletNftActivity : BaseActivity(R.layout.activity_connect, R.string.title_add_wallet) { - private var accountRepository: AccountRepository? = null + private val accountRepository = + AccountRepository(App.appCore.session.walletStorage.database.accountDao()) private var siteInfo: WsConnectionInfo.SiteInfo? = null private var payload: WsMessageResponse.Payload? = null @@ -48,9 +49,6 @@ class AddWalletNftActivity : BaseActivity(R.layout.activity_connect, R.string.ti descTitle.setText(R.string.title_add_wallet) accountsPool = findViewById(R.id.accountsPool) - - val accountDao = WalletDatabase.getDatabase(application).accountDao() - accountRepository = AccountRepository(accountDao) } override fun onResume() { @@ -69,7 +67,8 @@ class AddWalletNftActivity : BaseActivity(R.layout.activity_connect, R.string.ti shopName.text = siteInfo?.title shopDescription.text = siteInfo?.description - Glide.with(applicationContext).load(siteInfo?.iconLink).placeholder(R.drawable.ic_favicon).error(R.drawable.ic_favicon).into(shopLogo) + Glide.with(applicationContext).load(siteInfo?.iconLink).placeholder(R.drawable.ic_favicon) + .error(R.drawable.ic_favicon).into(shopLogo) findViewById