diff --git a/app/src/main/java/dev/medzik/librepass/android/MainActivity.kt b/app/src/main/java/dev/medzik/librepass/android/MainActivity.kt index d8045b38..1d15ece1 100644 --- a/app/src/main/java/dev/medzik/librepass/android/MainActivity.kt +++ b/app/src/main/java/dev/medzik/librepass/android/MainActivity.kt @@ -9,12 +9,12 @@ import androidx.fragment.app.FragmentActivity import androidx.navigation.NavController import dagger.hilt.android.AndroidEntryPoint import dev.medzik.android.utils.openEmailApplication +import dev.medzik.librepass.android.common.VaultCache import dev.medzik.librepass.android.common.popUpToDestination import dev.medzik.librepass.android.database.Repository import dev.medzik.librepass.android.ui.LibrePassNavigation import dev.medzik.librepass.android.ui.screens.auth.Unlock import dev.medzik.librepass.android.ui.theme.LibrePassTheme -import dev.medzik.librepass.android.utils.Vault import org.apache.commons.lang3.exception.ExceptionUtils import javax.inject.Inject @@ -24,7 +24,7 @@ class MainActivity : FragmentActivity() { lateinit var repository: Repository @Inject - lateinit var vault: Vault + lateinit var vault: VaultCache override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -46,8 +46,8 @@ class MainActivity : FragmentActivity() { MigrationsManager.run(this, repository) - // retrieves aes key to decrypt vault if key is valid - vault.getVaultSecrets(this) + // retrieves aes key for vault decryption if key is valid + vault.getSecretsIfNotExpired(this) setContent { LibrePassTheme( diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/LibrePassViewModel.kt b/app/src/main/java/dev/medzik/librepass/android/ui/LibrePassViewModel.kt index 96b69d8f..a4e74f59 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/LibrePassViewModel.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/LibrePassViewModel.kt @@ -2,14 +2,14 @@ package dev.medzik.librepass.android.ui import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import dev.medzik.librepass.android.common.VaultCache import dev.medzik.librepass.android.database.CredentialsDao import dev.medzik.librepass.android.database.LocalCipherDao -import dev.medzik.librepass.android.utils.Vault import javax.inject.Inject @HiltViewModel class LibrePassViewModel @Inject constructor( val cipherRepository: LocalCipherDao, val credentialRepository: CredentialsDao, - val vault: Vault + val vault: VaultCache ) : ViewModel() diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherEdit.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherEdit.kt index 115e3fd1..0d96653c 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherEdit.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherEdit.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import java.util.Date +import java.util.UUID @Serializable data class CipherEdit(val cipherId: String) @@ -51,7 +52,7 @@ fun CipherEditScreen( ) { val context = LocalContext.current - val oldCipher = remember { viewModel.vault.find(args.cipherId) } ?: return + val oldCipher = remember { viewModel.vault.find(UUID.fromString(args.cipherId)) } ?: return var cipher by rememberMutable(oldCipher) var loading by rememberMutableBoolean() diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherView.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherView.kt index d97566fe..8f8193eb 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherView.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherView.kt @@ -70,6 +70,7 @@ import kotlinx.coroutines.delay import kotlinx.serialization.Serializable import java.text.SimpleDateFormat import java.util.Locale +import java.util.UUID import java.util.concurrent.TimeUnit @Serializable @@ -81,7 +82,7 @@ fun CipherViewScreen( args: CipherView, viewModel: LibrePassViewModel = hiltViewModel() ) { - val cipher = remember { viewModel.vault.find(args.cipherId) } ?: return + val cipher = remember { viewModel.vault.find(UUID.fromString(args.cipherId)) } ?: return var totpCode by rememberMutable("") var totpElapsed by rememberMutable(0) diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/OtpConfigure.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/OtpConfigure.kt index d0a78864..df7d433a 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/OtpConfigure.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/OtpConfigure.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -36,6 +37,7 @@ import dev.medzik.otp.OTPParameters import dev.medzik.otp.OTPType import dev.medzik.otp.TOTPGenerator import kotlinx.serialization.Serializable +import java.util.UUID @Serializable data class OtpConfigure(val cipherId: String) @@ -46,7 +48,7 @@ fun OtpConfigureScreen( args: OtpConfigure, viewModel: LibrePassViewModel = hiltViewModel() ) { - val cipher = viewModel.vault.find(args.cipherId) + val cipher = remember { viewModel.vault.find(UUID.fromString(args.cipherId)) } ?: return Scaffold( topBar = { @@ -140,7 +142,7 @@ fun OtpConfigureScreen( } } else { var beginParams: OTPParameters? = null - if (cipher?.loginData?.twoFactor != null) { + if (cipher.loginData?.twoFactor != null) { beginParams = OTPParameters.parseUrl(cipher.loginData?.twoFactor) } diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Search.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Search.kt index 3d31d78c..089d79d8 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Search.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Search.kt @@ -7,9 +7,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -32,7 +38,7 @@ fun SearchScreen( navController: NavController, viewModel: LibrePassViewModel = hiltViewModel() ) { - val ciphers = viewModel.vault.sortAlphabetically() + val ciphers = remember { viewModel.vault.getSortedCiphers() } var searchText by rememberMutableString() diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Vault.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Vault.kt index adf89406..d8d0b3ca 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Vault.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Vault.kt @@ -10,10 +10,22 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.* +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.PullToRefreshContainer import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -42,7 +54,7 @@ import dev.medzik.librepass.client.api.CipherClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import java.util.* +import java.util.Date import java.util.concurrent.TimeUnit @Serializable @@ -55,14 +67,12 @@ fun VaultScreen( viewModel: LibrePassViewModel = hiltViewModel() ) { val context = LocalContext.current - val scope = rememberCoroutineScope() val pullToRefreshState = rememberPullToRefreshState() - var ciphers by remember { mutableStateOf(viewModel.vault.sortAlphabetically()) } - - val credentials = viewModel.credentialRepository.get() ?: return + var ciphers by remember { mutableStateOf(viewModel.vault.getSortedCiphers()) } + val credentials = remember { viewModel.credentialRepository.get() } ?: return val cipherClient = CipherClient( apiKey = credentials.apiKey, @@ -101,7 +111,7 @@ fun VaultScreen( } // sort ciphers and update UI - ciphers = viewModel.vault.sortAlphabetically() + ciphers = viewModel.vault.getSortedCiphers() } fun reSetupBiometrics() { @@ -163,11 +173,11 @@ fun VaultScreen( // decrypt database if needed if (viewModel.vault.ciphers.isEmpty()) { - viewModel.vault.decryptDatabase(dbCiphers) + viewModel.vault.decrypt(dbCiphers) } // sort ciphers and update UI - ciphers = viewModel.vault.sortAlphabetically() + ciphers = viewModel.vault.getSortedCiphers() // sync remote ciphers if (context.haveNetworkConnection()) { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 8e21cf1b..988d232e 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(libs.plugins.android.library) - alias(libs.plugins.compose.compiler) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.ksp) alias(libs.plugins.kotlin.parcelize) } @@ -23,7 +23,6 @@ android { } buildFeatures { - compose = true buildConfig = false } } @@ -33,5 +32,14 @@ dependencies { implementation(libs.compose.material3) implementation(libs.compose.navigation) implementation(libs.compose.ui) + implementation(libs.medzik.android.crypto) + implementation(libs.medzik.android.utils) + implementation(libs.librepass.client) + + implementation(libs.dagger.hilt) + implementation(libs.hilt.navigation.compose) + ksp(libs.dagger.hilt.compiler) + + implementation(projects.databaseLogic) } diff --git a/app/src/main/java/dev/medzik/librepass/android/utils/Vault.kt b/common/src/main/java/dev/medzik/librepass/android/common/VaultCache.kt similarity index 67% rename from app/src/main/java/dev/medzik/librepass/android/utils/Vault.kt rename to common/src/main/java/dev/medzik/librepass/android/common/VaultCache.kt index 5c784f3c..bb497301 100644 --- a/app/src/main/java/dev/medzik/librepass/android/utils/Vault.kt +++ b/common/src/main/java/dev/medzik/librepass/android/common/VaultCache.kt @@ -1,76 +1,71 @@ -package dev.medzik.librepass.android.utils +package dev.medzik.librepass.android.common import android.content.Context import dev.medzik.android.utils.runOnIOThread +import dev.medzik.libcrypto.Hex import dev.medzik.librepass.android.database.LocalCipher import dev.medzik.librepass.android.database.LocalCipherDao -import dev.medzik.librepass.android.database.datastore.* +import dev.medzik.librepass.android.database.datastore.SecretsStore +import dev.medzik.librepass.android.database.datastore.VaultTimeoutValue +import dev.medzik.librepass.android.database.datastore.deleteSecretsStore +import dev.medzik.librepass.android.database.datastore.readSecretsStore +import dev.medzik.librepass.android.database.datastore.readVaultTimeout +import dev.medzik.librepass.android.database.datastore.writeSecretsStore +import dev.medzik.librepass.android.database.datastore.writeVaultTimeout import dev.medzik.librepass.types.api.SyncResponse import dev.medzik.librepass.types.cipher.Cipher import dev.medzik.librepass.types.cipher.CipherType import dev.medzik.librepass.types.cipher.EncryptedCipher import kotlinx.coroutines.runBlocking -import org.apache.commons.codec.binary.Hex -import java.util.* +import java.util.UUID -class Vault( - private val cipherRepository: LocalCipherDao -) { +class VaultCache(private val cipherRepository: LocalCipherDao) { var aesKey: ByteArray = byteArrayOf() val ciphers = mutableListOf() - fun decryptDatabase(ciphers: List) { + fun decrypt(ciphers: List) { ciphers.forEach { val cipher = Cipher(it.encryptedCipher, aesKey) this.ciphers.add(cipher) } } - fun sync(syncResponse: SyncResponse) { + fun getSecretsIfNotExpired(context: Context) { + val expired = handleExpiration(context) + if (!expired) { + runOnIOThread { + aesKey = Hex.decode(readSecretsStore(context).aesKey) + } + } + } + + fun sync(response: SyncResponse) { val cacheCipherIDs: MutableList = mutableListOf() ciphers.forEach { cacheCipherIDs.add(it.id) } // delete ciphers from the local database that are not in API response for (cipherId in cacheCipherIDs) { - if (cipherId !in syncResponse.ids) { + if (cipherId !in response.ids) { delete(cipherId) } } // update ciphers - for (cipher in syncResponse.ciphers) { + for (cipher in response.ciphers) { save(cipher, needUpload = false) } } - fun sortAlphabetically(): List { - return ciphers.sortedBy { - when (it.type) { - CipherType.Login -> { - it.loginData!!.name - } - CipherType.SecureNote -> { - it.secureNoteData!!.title - } - CipherType.Card -> { - it.cardData!!.name - } - } - } - } - - fun find(id: String): Cipher? = find(UUID.fromString(id)) - fun find(id: UUID): Cipher? = ciphers.find { it.id == id } - fun filterByURI(uri: String): List = ciphers.filter { it.loginData?.uris?.contains(uri) == true } - fun save( encryptedCipher: EncryptedCipher, needUpload: Boolean = true - ) { - return save(Cipher(encryptedCipher, aesKey), encryptedCipher, needUpload) - } + ) = save( + cipher = Cipher(encryptedCipher, aesKey), + encryptedCipher = encryptedCipher, + needUpload = needUpload + ) fun save( cipher: Cipher, @@ -81,7 +76,10 @@ class Vault( ciphers.add(cipher) cipherRepository.insert( - LocalCipher(encryptedCipher ?: EncryptedCipher(cipher, aesKey), needUpload) + LocalCipher( + encryptedCipher = encryptedCipher ?: EncryptedCipher(cipher, aesKey), + needUpload = needUpload + ) ) } @@ -90,29 +88,18 @@ class Vault( cipherRepository.delete(id) } - fun getVaultSecrets(context: Context) { - val expired = handleExpiration(context) - if (!expired) { - runOnIOThread { - aesKey = Hex.decodeHex(readSecretsStore(context).aesKey) - } - } - } - - fun saveVaultExpiration(context: Context) { - val vaultTimeout = runBlocking { readVaultTimeout(context) } - - if (vaultTimeout.timeout == VaultTimeoutValue.INSTANT) { - deleteSecrets(context) - } else { - runOnIOThread { - writeSecretsStore(context, SecretsStore(Hex.encodeHexString(aesKey))) - } - - if (vaultTimeout.timeout != VaultTimeoutValue.NEVER) { - val currentTime = System.currentTimeMillis() - val newExpiresTime = currentTime + (vaultTimeout.timeout.minutes * 60 * 1000) - runOnIOThread { writeVaultTimeout(context, vaultTimeout.copy(expires = newExpiresTime)) } + fun getSortedCiphers(): List { + return ciphers.sortedBy { + when (it.type) { + CipherType.Login -> { + it.loginData!!.name + } + CipherType.SecureNote -> { + it.secureNoteData!!.title + } + CipherType.Card -> { + it.cardData!!.name + } } } } @@ -133,9 +120,31 @@ class Vault( return false } + fun saveVaultExpiration(context: Context) { + val vaultTimeout = runBlocking { readVaultTimeout(context) } + + if (vaultTimeout.timeout == VaultTimeoutValue.INSTANT) { + deleteSecrets(context) + } else { + runOnIOThread { + writeSecretsStore(context, SecretsStore(Hex.encode(aesKey))) + } + + if (vaultTimeout.timeout != VaultTimeoutValue.NEVER) { + val currentTime = System.currentTimeMillis() + val newExpiresTime = currentTime + (vaultTimeout.timeout.minutes * 60 * 1000) + runOnIOThread { + writeVaultTimeout(context, vaultTimeout.copy(expires = newExpiresTime)) + } + } + } + } + fun deleteSecrets(context: Context) { aesKey = byteArrayOf() - runOnIOThread { deleteSecretsStore(context) } + runOnIOThread { + deleteSecretsStore(context) + } } } diff --git a/app/src/main/java/dev/medzik/librepass/android/injection/VaultModule.kt b/common/src/main/java/dev/medzik/librepass/android/common/injection/VaultCacheModule.kt similarity index 54% rename from app/src/main/java/dev/medzik/librepass/android/injection/VaultModule.kt rename to common/src/main/java/dev/medzik/librepass/android/common/injection/VaultCacheModule.kt index 9b3e3bad..c136b0b6 100644 --- a/app/src/main/java/dev/medzik/librepass/android/injection/VaultModule.kt +++ b/common/src/main/java/dev/medzik/librepass/android/common/injection/VaultCacheModule.kt @@ -1,19 +1,19 @@ -package dev.medzik.librepass.android.injection +package dev.medzik.librepass.android.common.injection import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dev.medzik.librepass.android.common.VaultCache import dev.medzik.librepass.android.database.LocalCipherDao -import dev.medzik.librepass.android.utils.Vault import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -object VaultModule { +object VaultCacheModule { @Singleton @Provides - fun providesVault(cipherRepository: LocalCipherDao): Vault { - return Vault(cipherRepository) + fun providesVault(cipherRepository: LocalCipherDao): VaultCache { + return VaultCache(cipherRepository) } }