├── common
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── dev
│ │ └── medzik
│ │ └── librepass
│ │ └── android
│ │ └── common
│ │ ├── Network.kt
│ │ ├── parceler
│ │ └── CipherType.kt
│ │ ├── LibrePassViewModel.kt
│ │ ├── Navigation.kt
│ │ └── navtype
│ │ └── CipherType.kt
└── build.gradle.kts
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── resources.properties
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ └── themes.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── values-vi
│ │ │ │ └── strings.xml
│ │ │ ├── values-nb-rNO
│ │ │ │ └── strings.xml
│ │ │ ├── values-ar
│ │ │ │ └── strings.xml
│ │ │ ├── values-hi
│ │ │ │ └── strings.xml
│ │ │ ├── values-tr
│ │ │ │ └── strings.xml
│ │ │ └── values-de
│ │ │ │ └── strings.xml
│ │ ├── java
│ │ │ └── dev
│ │ │ │ └── medzik
│ │ │ │ └── librepass
│ │ │ │ └── android
│ │ │ │ ├── utils
│ │ │ │ ├── KeyAlias.kt
│ │ │ │ ├── ShortenName.kt
│ │ │ │ ├── Exception.kt
│ │ │ │ └── Biometric.kt
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── screens
│ │ │ │ │ ├── settings
│ │ │ │ │ │ ├── account
│ │ │ │ │ │ │ ├── Utils.kt
│ │ │ │ │ │ │ ├── DeleteAccount.kt
│ │ │ │ │ │ │ ├── ChangeEmail.kt
│ │ │ │ │ │ │ └── ChangePassword.kt
│ │ │ │ │ │ ├── Settings.kt
│ │ │ │ │ │ ├── Navigation.kt
│ │ │ │ │ │ ├── SettingsAccount.kt
│ │ │ │ │ │ └── SettingsSecurity.kt
│ │ │ │ │ ├── auth
│ │ │ │ │ │ ├── Navigation.kt
│ │ │ │ │ │ ├── AddCustomServer.kt
│ │ │ │ │ │ ├── Register.kt
│ │ │ │ │ │ └── Login.kt
│ │ │ │ │ ├── vault
│ │ │ │ │ │ ├── Navigation.kt
│ │ │ │ │ │ ├── Search.kt
│ │ │ │ │ │ ├── CipherAdd.kt
│ │ │ │ │ │ └── CipherEdit.kt
│ │ │ │ │ └── Welcome.kt
│ │ │ │ ├── components
│ │ │ │ │ ├── CipherTypeDialog.kt
│ │ │ │ │ ├── QrScanner.kt
│ │ │ │ │ ├── TopAppBar.kt
│ │ │ │ │ ├── auth
│ │ │ │ │ │ └── ChoiceServer.kt
│ │ │ │ │ └── TextInputField.kt
│ │ │ │ └── Navigation.kt
│ │ │ │ ├── LibrePassApplication.kt
│ │ │ │ ├── MigrationsManager.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ └── debug
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── ic_launcher-playstore.png
├── proguard-rules.pro
└── build.gradle.kts
├── business-logic
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── dev
│ │ └── medzik
│ │ └── librepass
│ │ └── android
│ │ └── business
│ │ ├── injection
│ │ └── VaultCacheModule.kt
│ │ ├── SyncCiphers.kt
│ │ └── VaultCache.kt
└── build.gradle.kts
├── database-logic
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── dev
│ │ └── medzik
│ │ └── librepass
│ │ └── android
│ │ └── database
│ │ ├── datastore
│ │ ├── DataStoreKeyAlias.kt
│ │ ├── SecretsStore.kt
│ │ ├── AppVersion.kt
│ │ ├── CustomServers.kt
│ │ ├── PasswordGeneratorPreference.kt
│ │ └── VaultTimeout.kt
│ │ ├── Database.kt
│ │ ├── Repository.kt
│ │ ├── Credentials.kt
│ │ ├── DatabaseMigrations.kt
│ │ ├── DatabaseProvider.kt
│ │ ├── CredentialsDao.kt
│ │ ├── injection
│ │ └── RoomModule.kt
│ │ ├── LocalCipher.kt
│ │ └── LocalCipherDao.kt
└── build.gradle.kts
├── .github
├── FUNDING.yml
├── scripts
│ └── get-changelog.sh
├── dependabot.yml
└── workflows
│ ├── build.yml
│ └── release.yml
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── title.txt
│ ├── changelogs
│ ├── 18.txt
│ ├── 14.txt
│ ├── 9.txt
│ ├── 3.txt
│ ├── 16.txt
│ ├── 6.txt
│ ├── 15.txt
│ ├── 11.txt
│ ├── 10.txt
│ ├── 7.txt
│ ├── 13.txt
│ ├── 4.txt
│ ├── 8.txt
│ ├── 17.txt
│ ├── 5.txt
│ └── 12.txt
│ ├── short_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 01.png
│ │ ├── 02.png
│ │ ├── 03.png
│ │ └── 04.png
│ └── full_description.txt
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── settings.gradle.kts
├── gradle.properties
├── README.md
└── gradlew.bat
/common/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
--------------------------------------------------------------------------------
/business-logic/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/database-logic/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | liberapay: Medzik
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | LibrePass
--------------------------------------------------------------------------------
/app/src/main/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale=en-US
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/18.txt:
--------------------------------------------------------------------------------
1 | - Added deprecation warning
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Take control of your passwords
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/14.txt:
--------------------------------------------------------------------------------
1 | * Fixed crashing when biometric unlocking failed
2 |
--------------------------------------------------------------------------------
/app/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/9.txt:
--------------------------------------------------------------------------------
1 | - Fixed F-Droid build (issue #61)
2 | - Fixed string in Polish translation
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/3.txt:
--------------------------------------------------------------------------------
1 | - Added support for self-hosted servers.
2 | - Added translations for the German language.
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | LP - Dev
3 |
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/16.txt:
--------------------------------------------------------------------------------
1 | * Fixed crashing on some devices - disabled device authentication for
2 | * Bump AGP to 8.3.0
3 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/6.txt:
--------------------------------------------------------------------------------
1 | - Fixed the view collapsing when the keyboard is displayed
2 | - Added navigation bar padding to BottomSheet
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/15.txt:
--------------------------------------------------------------------------------
1 | * Don't run keystore encryption in coroutine
2 | * Don't show toast with "Network error" after app launch
3 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/business-logic/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/database-logic/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #E36811
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/11.txt:
--------------------------------------------------------------------------------
1 | - Added Arabic translation
2 | - Fixed inconsistent navigation status after application resume
3 | - Bump Gradle and compose compiler
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/02.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/03.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibrePass/LibrePass-Android/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/04.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
12 | *.keystore
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/10.txt:
--------------------------------------------------------------------------------
1 | - Refactor translations
2 | - Fixed text size on smaller screens in CipherCard
3 | - Updated LibrePass Client (new production API url is `api.librepass.org`)
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/7.txt:
--------------------------------------------------------------------------------
1 | - Fixed padding for keyboard
2 | - Only allows using biometric when it is available
3 | - Allows biometric authentication to be enabled immediately after login
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/13.txt:
--------------------------------------------------------------------------------
1 | * HotFix: Fixed crashing when editing cipher by manual adding otp
2 | * Fixed showing other cipher data (e.g website or 2fa) when cipher doesn't have username and password
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/4.txt:
--------------------------------------------------------------------------------
1 | - Fixed crashing in edit screen
2 | - Added padding for keyboard
3 | - Re-design settings screen
4 | - Added password history
5 | - Disable return to previous screen after vault is locked
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/utils/KeyAlias.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.utils
2 |
3 | import dev.medzik.android.crypto.KeyStoreAlias
4 |
5 | enum class KeyAlias : KeyStoreAlias {
6 | BiometricAesKey
7 | }
8 |
--------------------------------------------------------------------------------
/.github/scripts/get-changelog.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | VERSION_CODE=$(sed -n 's/.*versionCode\s*=\s*\([0-9]\+\).*/\1/p' app/build.gradle.kts)
4 | CHANGELOG=$(cat fastlane/metadata/android/en-US/changelogs/$VERSION_CODE.txt)
5 |
6 | echo "$CHANGELOG"
7 |
--------------------------------------------------------------------------------
/common/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 |
5 | // Set of Material typography styles to start with
6 | val Typography = Typography()
7 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/LibrePassApplication.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class LibrePassApplication : Application()
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/8.txt:
--------------------------------------------------------------------------------
1 | - Add support for secure notes and cards
2 | - Add support to change password and delete account from settings
3 | - Updated translations and added new language (Norwegian Bokmal)
4 | - Add support for per-app language preference
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/datastore/DataStoreKeyAlias.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database.datastore
2 |
3 | import dev.medzik.android.crypto.KeyStoreAlias
4 |
5 | internal object DataStoreKeyAlias : KeyStoreAlias {
6 | override val name = "DataStore"
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/17.txt:
--------------------------------------------------------------------------------
1 | - Updated translations.
2 | - Bumped dependencies.
3 | - Changed icon for password generator.
4 | - Better navigation animations.
5 | - Fixed network error toast by ignoring network errors.
6 | - Fixed copying otp code, copy it to clipboard without space.
7 | - Moved OTP field position, now it is under password field.
8 | - Optimized API and changed to the new sync API to minimize network usage.
9 |
--------------------------------------------------------------------------------
/common/src/main/java/dev/medzik/librepass/android/common/Network.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.common
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 |
6 | fun Context.haveNetworkConnection(): Boolean {
7 | val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
8 | val activeNetwork = connectivityManager.activeNetwork
9 | return activeNetwork != null
10 | }
11 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/5.txt:
--------------------------------------------------------------------------------
1 | - Add button in AuthScreen to get password hint
2 | - Translate "server address" string in auth screens
3 | - Refactor application
4 | - Use material3 with dynamic colors in splash screen
5 | - Add Pure Black (AMOLED) theme
6 | - Refactor navigation logic
7 | - Add search bar
8 | - Add support for merging application data to prevent crashes
9 | - Add spacer on the floating action button to completely display the last entry
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/utils/ShortenName.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.utils
2 |
3 | const val SHORTEN_NAME_LENGTH = 16
4 | const val SHORTEN_USERNAME_LENGTH = 20
5 |
6 | /**
7 | * Returns a string shortened to the specified length.
8 | *
9 | * @param length Length of characters to which it will be shortened.
10 | * @return The shortened string.
11 | */
12 | fun String.shorten(length: Int) = if (this.length > length) take(length) + "..." else this
13 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/Database.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 |
6 | @Database(
7 | version = 2,
8 | entities = [Credentials::class, LocalCipher::class],
9 | exportSchema = false
10 | )
11 | abstract class LibrePassDatabase : RoomDatabase() {
12 | abstract fun credentialsDao(): CredentialsDao
13 |
14 | abstract fun cipherDao(): LocalCipherDao
15 | }
16 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | # Maintain dependencies for GitHub Actions
5 | - package-ecosystem: "github-actions"
6 | directory: "/"
7 | schedule:
8 | interval: "weekly"
9 |
10 | # Maintain dependencies for Maven
11 | - package-ecosystem: "gradle"
12 | directory: "/"
13 | schedule:
14 | interval: "weekly"
15 | groups:
16 | kotlin:
17 | patterns:
18 | - "com.google.devtools.ksp"
19 | - "org.jetbrains.kotlin.android"
20 |
--------------------------------------------------------------------------------
/common/src/main/java/dev/medzik/librepass/android/common/parceler/CipherType.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.common.parceler
2 |
3 | import android.os.Parcel
4 | import dev.medzik.librepass.types.cipher.CipherType
5 | import kotlinx.parcelize.Parceler
6 |
7 | object CipherTypeParceler : Parceler {
8 | override fun create(parcel: Parcel) = CipherType.from(parcel.readInt())
9 |
10 | override fun CipherType.write(parcel: Parcel, flags: Int) {
11 | parcel.writeInt(ordinal)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/12.txt:
--------------------------------------------------------------------------------
1 | * Added support for generating one-time passwords (two-factor)
2 | * Changed PullRefresh to Material3
3 | * Added dependency injector
4 | * Refactored room database
5 | * Fixed crash after adding/removing fingerprint
6 | * Added support for adding ciphers offline
7 | * Added support for changing email address
8 | * Improved font for displaying password
9 | * Added basic crash reporter
10 | * Fixed possibly crashes - biometrics must support BIOMETRIC_STRONG
11 | * Bumped dependencies
12 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | @Suppress("UnstableApiUsage")
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | }
15 | }
16 |
17 | rootProject.name = "LibrePass"
18 |
19 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
20 |
21 | include(":app")
22 | include(":common")
23 | include(":database-logic")
24 | include(":business-logic")
25 |
--------------------------------------------------------------------------------
/common/src/main/java/dev/medzik/librepass/android/common/LibrePassViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.common
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.hilt.android.lifecycle.HiltViewModel
5 | import dev.medzik.librepass.android.business.VaultCache
6 | import dev.medzik.librepass.android.database.CredentialsDao
7 | import dev.medzik.librepass.android.database.LocalCipherDao
8 | import javax.inject.Inject
9 |
10 | @HiltViewModel
11 | class LibrePassViewModel @Inject constructor(
12 | val cipherRepository: LocalCipherDao,
13 | val credentialRepository: CredentialsDao,
14 | val vault: VaultCache
15 | ) : ViewModel()
16 |
--------------------------------------------------------------------------------
/common/src/main/java/dev/medzik/librepass/android/common/Navigation.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.common
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraph.Companion.findStartDestination
5 | import androidx.navigation.NavOptionsBuilder
6 |
7 | fun NavOptionsBuilder.popUpToStartDestination(navController: NavController) {
8 | popUpTo(navController.graph.findStartDestination().id) {
9 | saveState = false
10 | inclusive = true
11 | }
12 | }
13 |
14 | inline fun NavOptionsBuilder.popUpToDestination(destination: T) {
15 | popUpTo(destination) {
16 | saveState = false
17 | inclusive = true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/business-logic/src/main/java/dev/medzik/librepass/android/business/injection/VaultCacheModule.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.business.injection
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import dev.medzik.librepass.android.business.VaultCache
8 | import dev.medzik.librepass.android.database.LocalCipherDao
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | object VaultCacheModule {
14 | @Singleton
15 | @Provides
16 | fun providesVault(cipherRepository: LocalCipherDao): VaultCache {
17 | return VaultCache(cipherRepository)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/Repository.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database
2 |
3 | import android.content.Context
4 |
5 | /**
6 | * Repository interface provides database DAOs.
7 | */
8 | interface RepositoryInterface {
9 | val credentials: CredentialsDao
10 | val cipher: LocalCipherDao
11 | }
12 |
13 | /**
14 | * Repository class provides access to the database.
15 | */
16 | class Repository(context: Context) : RepositoryInterface {
17 | // get database instance
18 | private val database = DatabaseProvider.getInstance(context)
19 |
20 | override val credentials = database.credentialsDao()
21 | override val cipher = database.cipherDao()
22 | }
23 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/Credentials.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import java.util.*
6 |
7 | @Entity
8 | data class Credentials(
9 | @PrimaryKey
10 | val userId: UUID,
11 | val email: String,
12 | val apiUrl: String? = null,
13 | val apiKey: String,
14 | val publicKey: String,
15 | val lastSync: Long? = null,
16 | // argon2id parameters
17 | val memory: Int,
18 | val iterations: Int,
19 | val parallelism: Int,
20 | // for biometric auth
21 | val biometricAesKey: String? = null,
22 | val biometricAesKeyIV: String? = null,
23 | val biometricReSetup: Boolean = false
24 | )
25 |
--------------------------------------------------------------------------------
/common/src/main/java/dev/medzik/librepass/android/common/navtype/CipherType.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.common.navtype
2 |
3 | import android.os.Bundle
4 | import androidx.navigation.NavType
5 | import dev.medzik.librepass.types.cipher.CipherType
6 |
7 | val CipherTypeType = object : NavType(
8 | isNullableAllowed = false
9 | ) {
10 | override fun get(bundle: Bundle, key: String): CipherType {
11 | val ordinal = bundle.getInt(key)
12 | return CipherType.from(ordinal)
13 | }
14 |
15 | override fun parseValue(value: String): CipherType {
16 | return CipherType.from(value.toInt())
17 | }
18 |
19 | override fun serializeAsValue(value: CipherType): String {
20 | return value.ordinal.toString()
21 | }
22 |
23 | override fun put(bundle: Bundle, key: String, value: CipherType) {
24 | bundle.putInt(key, value.ordinal)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/Utils.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.settings.account
2 |
3 | import androidx.navigation.NavController
4 | import dev.medzik.android.utils.runOnUiThread
5 | import dev.medzik.librepass.android.common.LibrePassViewModel
6 | import dev.medzik.librepass.android.common.popUpToDestination
7 | import dev.medzik.librepass.android.ui.screens.Welcome
8 | import kotlinx.coroutines.runBlocking
9 | import java.util.UUID
10 |
11 | fun navigateToWelcomeAndLogout(
12 | viewModel: LibrePassViewModel,
13 | navController: NavController,
14 | userId: UUID
15 | ) {
16 | runBlocking {
17 | viewModel.credentialRepository.drop()
18 | viewModel.cipherRepository.drop(userId)
19 | }
20 |
21 | runOnUiThread {
22 | navController.navigate(Welcome) {
23 | popUpToDestination(Welcome)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/DatabaseMigrations.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database
2 |
3 | import androidx.room.migration.Migration
4 | import androidx.sqlite.db.SupportSQLiteDatabase
5 |
6 | object DatabaseMigrations {
7 | val MIGRATION_1_2 = object : Migration(1, 2) {
8 | override fun migrate(db: SupportSQLiteDatabase) {
9 | // Credentials
10 | db.execSQL("ALTER TABLE Credentials RENAME COLUMN biometricProtectedPrivateKey to biometricAesKey")
11 | db.execSQL("ALTER TABLE Credentials RENAME COLUMN biometricProtectedPrivateKeyIV to biometricAesKeyIV")
12 | db.execSQL("ALTER TABLE Credentials RENAME COLUMN biometricEnabled to biometricReSetup")
13 |
14 | // LocalCipher
15 | db.execSQL("ALTER TABLE CipherTable RENAME TO LocalCipher")
16 | db.execSQL("ALTER TABLE LocalCipher ADD COLUMN needUpload INTEGER NOT NULL DEFAULT 0")
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/DatabaseProvider.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 |
6 | /**
7 | * Database provider singleton class.
8 | */
9 | object DatabaseProvider {
10 | private var database: LibrePassDatabase? = null
11 |
12 | /**
13 | * Get database instance. If database is not initialized, it will be initialize.
14 | *
15 | * @param context application context
16 | * @return Database instance.
17 | */
18 | fun getInstance(context: Context): LibrePassDatabase {
19 | if (database == null) {
20 | database = Room.databaseBuilder(
21 | context,
22 | LibrePassDatabase::class.java,
23 | "librepass.db"
24 | )
25 | .addMigrations(DatabaseMigrations.MIGRATION_1_2)
26 | .allowMainThreadQueries()
27 | .build()
28 | }
29 |
30 | return database as LibrePassDatabase
31 | }
32 | }
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'README.md'
7 |
8 | pull_request:
9 |
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: Set up JDK 17
21 | uses: actions/setup-java@v4
22 | with:
23 | java-version: '17'
24 | distribution: 'temurin'
25 | cache: gradle
26 |
27 | - name: Setup Android SDK
28 | uses: android-actions/setup-android@v3
29 |
30 | - name: Build with Gradle
31 | run: ./gradlew build
32 |
33 | - name: Upload debug artifact
34 | uses: actions/upload-artifact@v4
35 | with:
36 | name: app-debug
37 | path: ./app/build/outputs/apk/debug/*.apk
38 |
39 | - name: Upload release artifact
40 | uses: actions/upload-artifact@v4
41 | with:
42 | name: app-release
43 | path: ./app/build/outputs/apk/release/*.apk
--------------------------------------------------------------------------------
/business-logic/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.library)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.kotlin.ksp)
5 | alias(libs.plugins.kotlin.parcelize)
6 | }
7 |
8 | android {
9 | namespace = "dev.medzik.librepass.android.business"
10 | compileSdk = libs.versions.android.sdk.compile.get().toInt()
11 |
12 | defaultConfig {
13 | minSdk = libs.versions.android.sdk.min.get().toInt()
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_17
18 | targetCompatibility = JavaVersion.VERSION_17
19 | }
20 |
21 | kotlinOptions {
22 | jvmTarget = JavaVersion.VERSION_17.toString()
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation(libs.librepass.client)
28 | implementation(libs.medzik.android.crypto)
29 | implementation(libs.medzik.android.utils)
30 |
31 | implementation(libs.dagger.hilt)
32 | implementation(libs.hilt.navigation.compose)
33 | ksp(libs.dagger.hilt.compiler)
34 |
35 | implementation(projects.databaseLogic)
36 | }
37 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/CredentialsDao.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.Query
6 | import androidx.room.Update
7 |
8 | /**
9 | * Data access object for [Credentials].
10 | */
11 | @Dao
12 | interface CredentialsDao {
13 | /**
14 | * Insert credentials into the database.
15 | * @param credentials the credentials to be inserted
16 | */
17 | @Insert
18 | suspend fun insert(credentials: Credentials)
19 |
20 | /**
21 | * Get credentials from the database.
22 | * @return The credentials from the database, if any.
23 | */
24 | @Query("SELECT * FROM credentials LIMIT 1")
25 | fun get(): Credentials?
26 |
27 | /**
28 | * Update credentials in the database.
29 | * @param credentials the updated credentials
30 | */
31 | @Update
32 | suspend fun update(credentials: Credentials)
33 |
34 | /**
35 | * Delete credentials from the database.
36 | */
37 | @Query("DELETE FROM credentials")
38 | suspend fun drop()
39 | }
40 |
--------------------------------------------------------------------------------
/common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.library)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.kotlin.ksp)
5 | alias(libs.plugins.kotlin.parcelize)
6 | }
7 |
8 | android {
9 | namespace = "dev.medzik.librepass.android.common"
10 | compileSdk = libs.versions.android.sdk.compile.get().toInt()
11 |
12 | defaultConfig {
13 | minSdk = libs.versions.android.sdk.min.get().toInt()
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_17
18 | targetCompatibility = JavaVersion.VERSION_17
19 | }
20 |
21 | kotlinOptions {
22 | jvmTarget = JavaVersion.VERSION_17.toString()
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation(libs.compose.navigation)
28 | implementation(libs.medzik.android.crypto)
29 | implementation(libs.medzik.android.utils)
30 |
31 | implementation(libs.librepass.client)
32 |
33 | implementation(libs.dagger.hilt)
34 | implementation(libs.hilt.navigation.compose)
35 | ksp(libs.dagger.hilt.compiler)
36 |
37 | implementation(projects.databaseLogic)
38 | implementation(projects.businessLogic)
39 | }
40 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/injection/RoomModule.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database.injection
2 |
3 | import android.content.Context
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import dagger.hilt.components.SingletonComponent
9 | import dev.medzik.librepass.android.database.CredentialsDao
10 | import dev.medzik.librepass.android.database.LocalCipherDao
11 | import dev.medzik.librepass.android.database.Repository
12 | import javax.inject.Singleton
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | object RoomModule {
17 | @Singleton
18 | @Provides
19 | fun providesRepository(
20 | @ApplicationContext context: Context
21 | ): Repository {
22 | return Repository(context)
23 | }
24 |
25 | @Singleton
26 | @Provides
27 | fun providesCipherRepository(repository: Repository): LocalCipherDao {
28 | return repository.cipher
29 | }
30 |
31 | @Singleton
32 | @Provides
33 | fun provideCredentialRepository(repository: Repository): CredentialsDao {
34 | return repository.credentials
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database-logic/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.library)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.kotlin.ksp)
5 | alias(libs.plugins.kotlin.serialization)
6 | }
7 |
8 | android {
9 | namespace = "dev.medzik.librepass.android.database"
10 | compileSdk = libs.versions.android.sdk.compile.get().toInt()
11 |
12 | defaultConfig {
13 | minSdk = libs.versions.android.sdk.min.get().toInt()
14 | }
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_17
18 | targetCompatibility = JavaVersion.VERSION_17
19 | }
20 |
21 | kotlinOptions {
22 | jvmTarget = JavaVersion.VERSION_17.toString()
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation(libs.androidx.room.ktx)
28 | implementation(libs.androidx.room.runtime)
29 | ksp(libs.androidx.room.compiler)
30 |
31 | implementation(libs.androidx.datastore)
32 | implementation(libs.kotlinx.serialization.json)
33 |
34 | implementation(libs.dagger.hilt)
35 | implementation(libs.hilt.navigation.compose)
36 | ksp(libs.dagger.hilt.compiler)
37 |
38 | implementation(libs.medzik.android.crypto)
39 | implementation(libs.librepass.client)
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/LocalCipher.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import androidx.room.TypeConverter
6 | import androidx.room.TypeConverters
7 | import dev.medzik.librepass.client.utils.JsonUtils
8 | import dev.medzik.librepass.types.cipher.EncryptedCipher
9 | import java.util.*
10 |
11 | @Entity
12 | class LocalCipher(
13 | @PrimaryKey
14 | val id: UUID,
15 | val owner: UUID,
16 | val needUpload: Boolean,
17 | @field:TypeConverters(EncryptedCipherConverter::class)
18 | var encryptedCipher: EncryptedCipher
19 | ) {
20 | constructor(
21 | encryptedCipher: EncryptedCipher,
22 | needUpload: Boolean = false
23 | ) : this(
24 | id = encryptedCipher.id,
25 | owner = encryptedCipher.owner,
26 | needUpload = needUpload,
27 | encryptedCipher = encryptedCipher
28 | )
29 | }
30 |
31 | class EncryptedCipherConverter {
32 | @TypeConverter
33 | fun fromEncryptedCipher(encryptedCipher: EncryptedCipher): String {
34 | return JsonUtils.serialize(encryptedCipher)
35 | }
36 |
37 | @TypeConverter
38 | fun toEncryptedCipher(json: String): EncryptedCipher {
39 | return JsonUtils.deserialize(json)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.nonFinalResIds=false
25 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/Settings.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.settings
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Fingerprint
6 | import androidx.compose.material.icons.filled.ManageAccounts
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.res.stringResource
9 | import androidx.navigation.NavController
10 | import dev.medzik.android.compose.ui.IconBox
11 | import dev.medzik.android.compose.ui.preference.BasicPreference
12 | import dev.medzik.librepass.android.R
13 | import kotlinx.serialization.Serializable
14 |
15 | @Serializable
16 | object Settings
17 |
18 | @Composable
19 | fun SettingsScreen(navController: NavController) {
20 | Column {
21 | BasicPreference(
22 | leading = { IconBox(Icons.Default.Fingerprint) },
23 | title = stringResource(R.string.Settings_Security),
24 | subtitle = stringResource(R.string.Settings_Security_Subtitle),
25 | onClick = { navController.navigate(SettingsSecurity) }
26 | )
27 |
28 | BasicPreference(
29 | leading = { IconBox(Icons.Default.ManageAccounts) },
30 | title = stringResource(R.string.Settings_Account),
31 | subtitle = stringResource(R.string.Settings_Account_Subtitle),
32 | onClick = { navController.navigate(SettingsAccount) }
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/auth/Navigation.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.auth
2 |
3 | import androidx.compose.ui.res.stringResource
4 | import androidx.navigation.NavController
5 | import androidx.navigation.NavGraphBuilder
6 | import androidx.navigation.compose.composable
7 | import dev.medzik.librepass.android.R
8 | import dev.medzik.librepass.android.ui.DefaultScaffold
9 | import dev.medzik.librepass.android.ui.TopBarWithBack
10 | import dev.medzik.librepass.android.ui.components.TopBar
11 |
12 | fun NavGraphBuilder.authNavigation(navController: NavController) {
13 | composable {
14 | DefaultScaffold(
15 | topBar = { TopBarWithBack(title = R.string.Register, navController) }
16 | ) {
17 | RegisterScreen(navController)
18 | }
19 | }
20 |
21 | composable {
22 | DefaultScaffold(
23 | topBar = { TopBarWithBack(title = R.string.Login, navController) }
24 | ) {
25 | LoginScreen(navController)
26 | }
27 | }
28 |
29 | composable {
30 | DefaultScaffold(
31 | topBar = { TopBar(title = stringResource(R.string.Unlock)) }
32 | ) {
33 | UnlockScreen(navController)
34 | }
35 | }
36 |
37 | composable {
38 | DefaultScaffold(
39 | topBar = { TopBarWithBack(title = R.string.AddServer, navController) }
40 | ) {
41 | AddCustomServerScreen(navController)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/components/CipherTypeDialog.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.components
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.res.stringResource
9 | import androidx.compose.ui.unit.dp
10 | import dev.medzik.android.compose.ui.dialog.DialogState
11 | import dev.medzik.android.compose.ui.dialog.PickerDialog
12 | import dev.medzik.librepass.android.R
13 | import dev.medzik.librepass.types.cipher.CipherType
14 |
15 | @Composable
16 | fun CipherTypeDialog(
17 | state: DialogState,
18 | onSelected: (CipherType) -> Unit
19 | ) {
20 | @Composable
21 | fun getTranslated(type: CipherType): String {
22 | return stringResource(
23 | when (type) {
24 | CipherType.Login -> R.string.CipherType_Login
25 | CipherType.SecureNote -> R.string.CipherType_SecureNote
26 | CipherType.Card -> R.string.CipherType_Card
27 | }
28 | )
29 | }
30 |
31 | PickerDialog(
32 | state,
33 | title = stringResource(R.string.SelectCipherType),
34 | items = CipherType.entries,
35 | onSelected
36 | ) {
37 | Text(
38 | text = getTranslated(it),
39 | modifier = Modifier
40 | .padding(vertical = 12.dp)
41 | .fillMaxWidth()
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/business-logic/src/main/java/dev/medzik/librepass/android/business/SyncCiphers.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.business
2 |
3 | import android.content.Context
4 | import dev.medzik.librepass.android.database.Credentials
5 | import dev.medzik.librepass.android.database.injection.RoomModule
6 | import dev.medzik.librepass.client.api.CipherClient
7 | import java.util.Date
8 | import java.util.concurrent.TimeUnit
9 |
10 | suspend fun syncCiphers(
11 | context: Context,
12 | credentials: Credentials,
13 | client: CipherClient,
14 | vault: VaultCache
15 | ) {
16 | val currentTimeSeconds = TimeUnit.MILLISECONDS.toSeconds(Date().time)
17 |
18 | val repository = RoomModule.providesRepository(context)
19 |
20 | val localCiphers = repository.cipher.getAll(credentials.userId)
21 |
22 | val lastSync = credentials.lastSync ?: 0
23 | val lastSyncDate = Date(TimeUnit.SECONDS.toMillis(lastSync))
24 |
25 | val needUpload = localCiphers.filter { it.needUpload }.map { it.encryptedCipher }
26 | // TODO: delete ciphers using this method
27 | val syncResponse = client.sync(lastSyncDate, needUpload, emptyList())
28 |
29 | // last sync is zero for first sync
30 | if (lastSync != 0L) {
31 | // synchronize the local database with the server database
32 | vault.sync(syncResponse)
33 | } else {
34 | // save all ciphers
35 | for (cipher in syncResponse.ciphers) {
36 | vault.save(cipher)
37 | }
38 | }
39 |
40 | // update the last sync date
41 | repository.credentials.update(credentials.copy(lastSync = currentTimeSeconds))
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/components/QrScanner.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.components
2 |
3 | import android.app.Activity
4 | import android.util.Log
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.platform.LocalContext
11 | import androidx.compose.ui.unit.dp
12 | import androidx.compose.ui.viewinterop.AndroidView
13 | import com.journeyapps.barcodescanner.CaptureManager
14 | import com.journeyapps.barcodescanner.DecoratedBarcodeView
15 |
16 | @Composable
17 | fun QrCodeScanner(onScanned: (String) -> Unit) {
18 | val context = LocalContext.current
19 |
20 | val compoundBarcodeView =
21 | remember {
22 | DecoratedBarcodeView(context).apply {
23 | val capture = CaptureManager(context as Activity, this)
24 | capture.initializeFromIntent(context.intent, null)
25 | this.setStatusText("")
26 | this.resume()
27 | capture.decode()
28 | this.decodeContinuous { result ->
29 | result.text?.let { scannedText ->
30 | Log.i("QR_SCANNER", scannedText)
31 | onScanned.invoke(scannedText)
32 | }
33 | }
34 | }
35 | }
36 |
37 | AndroidView(
38 | modifier = Modifier
39 | .fillMaxWidth()
40 | .height(300.dp),
41 | factory = { compoundBarcodeView }
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Navigation.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.vault
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.compose.composable
6 | import androidx.navigation.toRoute
7 | import dev.medzik.librepass.android.common.navtype.CipherTypeType
8 | import dev.medzik.librepass.android.ui.DefaultScaffold
9 | import dev.medzik.librepass.types.cipher.CipherType
10 | import kotlin.reflect.typeOf
11 |
12 | fun NavGraphBuilder.vaultNavigation(navController: NavController) {
13 | composable {
14 | DefaultScaffold(
15 | topBar = { VaultScreenTopBar(navController) },
16 | floatingActionButton = { VaultScreenFloatingActionButton(navController) }
17 | ) {
18 | VaultScreen(navController)
19 | }
20 | }
21 |
22 | composable {
23 | val args = it.toRoute()
24 |
25 | CipherViewScreen(navController, args)
26 | }
27 |
28 | composable(
29 | typeMap = mapOf(typeOf() to CipherTypeType)
30 | ) {
31 | val args = it.toRoute()
32 |
33 | CipherAddScreen(navController, args)
34 | }
35 |
36 | composable {
37 | val args = it.toRoute()
38 |
39 | CipherEditScreen(navController, args)
40 | }
41 |
42 | composable {
43 | val args = it.toRoute()
44 |
45 | OtpConfigureScreen(navController, args)
46 | }
47 |
48 | composable {
49 | SearchScreen(navController)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/MigrationsManager.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android
2 |
3 | import android.content.Context
4 | import dev.medzik.librepass.android.database.Repository
5 | import dev.medzik.librepass.android.database.datastore.AppVersion
6 | import dev.medzik.librepass.android.database.datastore.readAppVersion
7 | import dev.medzik.librepass.android.database.datastore.writeAppVersion
8 | import kotlinx.coroutines.runBlocking
9 |
10 | object MigrationsManager {
11 | fun run(
12 | context: Context,
13 | repository: Repository
14 | ) {
15 | if (repository.credentials.get() == null) {
16 | runBlocking { writeAppVersion(context, AppVersion(BuildConfig.VERSION_CODE)) }
17 | return
18 | }
19 |
20 | val appVersion = readAppVersion(context)
21 | var versionCode = appVersion.lastVersionLaunched
22 |
23 | while (versionCode < BuildConfig.VERSION_CODE) {
24 | when (versionCode) {
25 | 0 -> disableBiometric(repository)
26 | }
27 |
28 | versionCode++
29 | }
30 |
31 | runBlocking { writeAppVersion(context, AppVersion(versionCode)) }
32 | }
33 |
34 | private fun disableBiometric(repository: Repository) {
35 | val credentials = repository.credentials.get() ?: return
36 |
37 | runBlocking {
38 | repository.credentials.update(
39 | credentials.copy(
40 | biometricReSetup = true,
41 | biometricAesKey = null,
42 | biometricAesKeyIV = null
43 | )
44 | )
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | LibrePass represents an innovative cloud-based password manager
2 | known for its user-friendly simplicity and performance.
3 | It functions not only as a tool for generating secure passwords
4 | but also facilitates their synchronization across various devices.
5 |
6 | Password leaks from online websites pose a significant threat,
7 | especially considering that most people use a limited number of passwords.
8 | Using the same passwords for multiple services carries the risk of unauthorized access,
9 | jeopardizing bank accounts, social media profiles, and other online services.
10 | Therefore, it is recommended to use long and complex passwords for each of our accounts.
11 | Nevertheless, maintaining control over multiple passwords can be challenging.
12 | LibrePass addresses this issue by providing a straightforward and enjoyable way to generate, store,
13 | and access passwords.
14 |
15 | All passwords stored in LibrePass are securely placed in an encrypted vault.
16 | This ensures that users can have full confidence
17 | that their passwords will remain private and inaccessible to unauthorized individuals.
18 |
19 | LibrePass is an open-source project, meaning the complete source code is available to everyone.
20 | This openness allows anyone to review, verify, and make their own changes,
21 | emphasizing the transparency and trust associated with using this tool.
22 |
23 |
Features:
24 |
25 | - Advanced password encryption
26 | - Generating secure passwords
27 | - Face and fingerprint unlocking
28 | - Native system theme (Android 12+)
29 | - Native performance
30 | - Various settings options
31 | - And more with incoming updates!
32 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | -keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | # ERROR: R8: Missing class lombok.NonNull
24 | -dontwarn lombok.**
25 |
26 | # Application classes that will be serialized/deserialized over Gson
27 | -keep class dev.medzik.librepass.types.** { *; }
28 |
29 | # Gson uses generic type information stored in a class file when working with
30 | # fields. Proguard removes such information by default, keep it.
31 | -keepattributes Signature
32 |
33 | # This is also needed for R8 in compat mode since multiple
34 | # optimizations will remove the generic signature such as class
35 | # merging and argument removal. See:
36 | # https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#troubleshooting-gson-gson
37 | -keep class com.google.gson.reflect.TypeToken { *; }
38 | -keep class * extends com.google.gson.reflect.TypeToken
39 |
40 | # Optional. For using GSON @Expose annotation
41 | -keepattributes AnnotationDefault,RuntimeVisibleAnnotations
42 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/datastore/SecretsStore.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database.datastore
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.dataStore
6 | import dev.medzik.android.crypto.EncryptedDataStore
7 | import dev.medzik.android.crypto.EncryptedDataStoreSerializer
8 | import kotlinx.coroutines.flow.first
9 | import kotlinx.coroutines.runBlocking
10 | import kotlinx.serialization.Serializable
11 | import kotlinx.serialization.encodeToString
12 | import kotlinx.serialization.json.Json
13 |
14 | @Serializable
15 | data class SecretsStore(
16 | val aesKey: String
17 | ) : EncryptedDataStore
18 |
19 | private object SecretsStoreSerializer : EncryptedDataStoreSerializer {
20 | override val defaultValue = SecretsStore("")
21 | override val keyStoreAlias = DataStoreKeyAlias
22 |
23 | override fun decode(str: String): SecretsStore {
24 | return Json.decodeFromString(str)
25 | }
26 |
27 | override fun encode(t: SecretsStore): String {
28 | return Json.encodeToString(t)
29 | }
30 | }
31 |
32 | private val Context.secretsDataStore: DataStore by dataStore(
33 | fileName = "secretsStore.pb",
34 | serializer = SecretsStoreSerializer
35 | )
36 |
37 | fun readSecretsStore(context: Context): SecretsStore {
38 | return runBlocking { context.secretsDataStore.data.first() }
39 | }
40 |
41 | suspend fun writeSecretsStore(context: Context, preference: SecretsStore) {
42 | context.secretsDataStore.updateData { preference }
43 | }
44 |
45 | suspend fun deleteSecretsStore(context: Context) {
46 | context.secretsDataStore.updateData { SecretsStore("") }
47 | }
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LibrePass Android
2 |
3 | > [!WARNING]
4 | > Deprecated
5 |
6 | LibrePass represents an innovative cloud-based password manager known for its user-friendly
7 | simplicity and performance.
8 | It functions not only as a tool for generating secure passwords
9 | but also facilitates their synchronization across various devices.
10 |
11 | All passwords stored in LibrePass are securely placed in an encrypted vault.
12 | This ensures that users can have full confidence
13 | that their passwords will remain private and inaccessible to unauthorized individuals.
14 |
15 |
16 |
17 | ## Install
18 |
19 | [
](https://android.izzysoft.de/repo/apk/dev.medzik.librepass.android)
20 | [
](https://f-droid.org/en/packages/dev.medzik.librepass.android)
21 |
22 | You can download and install .apk of LibrePass app from [releases](https://github.com/LibrePass/android/releases).
23 | It will be also available in Google Play Store soon.
24 |
25 | ## Features
26 |
27 | - Advanced password encryption
28 | - Generating secure passwords
29 | - Face and fingerprint unlocking
30 | - Native system theme (Android 12+)
31 | - Native performance
32 | - Various settings options
33 | - And more with incoming updates!
34 |
35 | ## 📝 Translations
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/datastore/AppVersion.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database.datastore
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.core.Serializer
6 | import androidx.datastore.dataStore
7 | import kotlinx.coroutines.flow.first
8 | import kotlinx.coroutines.runBlocking
9 | import kotlinx.serialization.ExperimentalSerializationApi
10 | import kotlinx.serialization.Serializable
11 | import kotlinx.serialization.json.Json
12 | import kotlinx.serialization.json.decodeFromStream
13 | import kotlinx.serialization.json.encodeToStream
14 | import java.io.InputStream
15 | import java.io.OutputStream
16 |
17 | @Serializable
18 | data class AppVersion(
19 | val lastVersionLaunched: Int
20 | )
21 |
22 | private object AppVersionSerializer : Serializer {
23 | override val defaultValue = AppVersion(0)
24 |
25 | @OptIn(ExperimentalSerializationApi::class)
26 | override suspend fun readFrom(input: InputStream): AppVersion {
27 | return Json.decodeFromStream(input)
28 | }
29 |
30 | @OptIn(ExperimentalSerializationApi::class)
31 | override suspend fun writeTo(
32 | t: AppVersion,
33 | output: OutputStream
34 | ) = Json.encodeToStream(t, output)
35 | }
36 |
37 | private val Context.appVersionDataStore: DataStore by dataStore(
38 | fileName = "appVersion.pb",
39 | serializer = AppVersionSerializer
40 | )
41 |
42 | fun readAppVersion(context: Context): AppVersion {
43 | return runBlocking { context.appVersionDataStore.data.first() }
44 | }
45 |
46 | suspend fun writeAppVersion(context: Context, preference: AppVersion) {
47 | context.appVersionDataStore.updateData { preference }
48 | }
49 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/datastore/CustomServers.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database.datastore
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.core.Serializer
6 | import androidx.datastore.dataStore
7 | import kotlinx.coroutines.flow.first
8 | import kotlinx.serialization.ExperimentalSerializationApi
9 | import kotlinx.serialization.Serializable
10 | import kotlinx.serialization.json.Json
11 | import kotlinx.serialization.json.decodeFromStream
12 | import kotlinx.serialization.json.encodeToStream
13 | import java.io.InputStream
14 | import java.io.OutputStream
15 |
16 | @Serializable
17 | data class CustomServers(
18 | val name: String,
19 | val address: String
20 | )
21 |
22 | private object CustomServersSerializer : Serializer> {
23 | override val defaultValue = emptyList()
24 |
25 | @OptIn(ExperimentalSerializationApi::class)
26 | override suspend fun readFrom(input: InputStream): List {
27 | return Json.decodeFromStream(input)
28 | }
29 |
30 | @OptIn(ExperimentalSerializationApi::class)
31 | override suspend fun writeTo(
32 | t: List,
33 | output: OutputStream
34 | ) = Json.encodeToStream(t, output)
35 | }
36 |
37 | private val Context.customServersDataStore: DataStore> by dataStore(
38 | fileName = "customServers.pb",
39 | serializer = CustomServersSerializer
40 | )
41 |
42 | suspend fun readCustomServers(context: Context): List {
43 | return context.customServersDataStore.data.first()
44 | }
45 |
46 | suspend fun writeCustomServers(context: Context, preference: List) {
47 | context.customServersDataStore.updateData { preference }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/components/TopAppBar.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.components
2 |
3 | import androidx.compose.foundation.layout.RowScope
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.MoreHoriz
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.IconButton
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TopAppBar
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.tooling.preview.Preview
15 | import androidx.navigation.NavController
16 | import dev.medzik.android.compose.icons.TopAppBarBackIcon
17 |
18 | @OptIn(ExperimentalMaterial3Api::class)
19 | @Composable
20 | fun TopBar(
21 | title: String,
22 | navigationIcon: @Composable (() -> Unit) = {},
23 | actions: @Composable (RowScope.() -> Unit) = {}
24 | ) {
25 | TopAppBar(
26 | title = {
27 | Text(
28 | text = title,
29 | style = MaterialTheme.typography.titleLarge
30 | )
31 | },
32 | navigationIcon = navigationIcon,
33 | actions = actions
34 | )
35 | }
36 |
37 | @Preview
38 | @Composable
39 | fun TopBarPreview() {
40 | TopBar(
41 | title = "Title",
42 | navigationIcon = {
43 | TopAppBarBackIcon(navController = NavController(LocalContext.current))
44 | },
45 | actions = {
46 | IconButton(onClick = {}) {
47 | Icon(
48 | imageVector = Icons.Default.MoreHoriz,
49 | contentDescription = null
50 | )
51 | }
52 | }
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/LocalCipherDao.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database
2 |
3 | import androidx.room.*
4 | import java.util.*
5 |
6 | /**
7 | * Data access object for [LocalCipher].
8 | */
9 | @Dao
10 | interface LocalCipherDao {
11 | /**
12 | * Get a cipher by id.
13 | * @param id cipher identifier
14 | * @return The cipher with the given id.
15 | */
16 | @Query("SELECT * FROM localCipher WHERE id = :id")
17 | fun get(id: UUID): LocalCipher?
18 |
19 | /**
20 | * Get all ciphers.
21 | * @param owner user identifier
22 | * @return All ciphers.
23 | */
24 | @Query("SELECT * FROM localCipher WHERE owner = :owner")
25 | fun getAll(owner: UUID): List
26 |
27 | /**
28 | * Get all ciphers ids.
29 | * @param owner user identifier
30 | * @return All ciphers ids.
31 | */
32 | @Query("SELECT id FROM localCipher WHERE owner = :owner")
33 | fun getAllIDs(owner: UUID): List
34 |
35 | /**
36 | * Delete a cipher by id.
37 | * @param id cipher identifier
38 | */
39 | @Query("DELETE FROM localCipher WHERE id = :id")
40 | fun delete(id: UUID)
41 |
42 | /**
43 | * Delete ciphers by ids.
44 | * @param ids cipher identifiers
45 | */
46 | @Query("DELETE FROM localCipher WHERE id IN (:ids)")
47 | fun delete(ids: List)
48 |
49 | /**
50 | * Insert a cipher. If the cipher already exists, replace it.
51 | * @param cipherTable cipher to be inserted
52 | * @return The id of the inserted cipher.
53 | */
54 | @Insert(onConflict = OnConflictStrategy.REPLACE)
55 | fun insert(cipherTable: LocalCipher)
56 |
57 | /**
58 | * Update a cipher.
59 | * @param cipherTable updated cipher
60 | */
61 | @Update
62 | fun update(cipherTable: LocalCipher)
63 |
64 | /**
65 | * Drop all ciphers owned by the given owner.
66 | * @param owner user identifier
67 | */
68 | @Query("DELETE FROM localCipher WHERE owner = :owner")
69 | suspend fun drop(owner: UUID)
70 | }
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/datastore/PasswordGeneratorPreference.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database.datastore
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.core.Serializer
6 | import androidx.datastore.dataStore
7 | import kotlinx.coroutines.flow.first
8 | import kotlinx.serialization.ExperimentalSerializationApi
9 | import kotlinx.serialization.Serializable
10 | import kotlinx.serialization.json.Json
11 | import kotlinx.serialization.json.decodeFromStream
12 | import kotlinx.serialization.json.encodeToStream
13 | import java.io.InputStream
14 | import java.io.OutputStream
15 |
16 | @Serializable
17 | data class PasswordGeneratorPreference(
18 | val length: Int = 16,
19 | val capitalize: Boolean = true,
20 | val includeNumbers: Boolean = true,
21 | val includeSymbols: Boolean = true
22 | )
23 |
24 | private object PasswordGeneratorPreferenceSerializer : Serializer {
25 | override val defaultValue = PasswordGeneratorPreference()
26 |
27 | @OptIn(ExperimentalSerializationApi::class)
28 | override suspend fun readFrom(input: InputStream): PasswordGeneratorPreference {
29 | return Json.decodeFromStream(input)
30 | }
31 |
32 | @OptIn(ExperimentalSerializationApi::class)
33 | override suspend fun writeTo(
34 | t: PasswordGeneratorPreference,
35 | output: OutputStream
36 | ) = Json.encodeToStream(t, output)
37 | }
38 |
39 | private val Context.passwordGeneratorPreferenceDataStore: DataStore by dataStore(
40 | fileName = "passwordGeneratorPreference.pb",
41 | serializer = PasswordGeneratorPreferenceSerializer
42 | )
43 |
44 | suspend fun readPasswordGeneratorPreference(context: Context): PasswordGeneratorPreference {
45 | return context.passwordGeneratorPreferenceDataStore.data.first()
46 | }
47 |
48 | suspend fun writePasswordGeneratorPreference(context: Context, preference: PasswordGeneratorPreference) {
49 | context.passwordGeneratorPreferenceDataStore.updateData { preference }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/Navigation.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.settings
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.compose.composable
6 | import dev.medzik.librepass.android.R
7 | import dev.medzik.librepass.android.ui.DefaultScaffold
8 | import dev.medzik.librepass.android.ui.TopBarWithBack
9 | import dev.medzik.librepass.android.ui.screens.settings.account.*
10 |
11 | fun NavGraphBuilder.settingsNavigation(navController: NavController) {
12 | composable {
13 | DefaultScaffold(
14 | topBar = { TopBarWithBack(title = R.string.Settings, navController) },
15 | horizontalPadding = false
16 | ) {
17 | SettingsScreen(navController)
18 | }
19 | }
20 |
21 | composable {
22 | DefaultScaffold(
23 | topBar = { TopBarWithBack(title = R.string.Settings_Security, navController) },
24 | horizontalPadding = false
25 | ) {
26 | SettingsSecurityScreen()
27 | }
28 | }
29 |
30 | composable {
31 | DefaultScaffold(
32 | topBar = { TopBarWithBack(title = R.string.Settings_Account, navController) },
33 | horizontalPadding = false
34 | ) {
35 | SettingsAccountScreen(navController)
36 | }
37 | }
38 |
39 | composable {
40 | DefaultScaffold(
41 | topBar = { TopBarWithBack(title = R.string.ChangeEmail, navController) }
42 | ) {
43 | SettingsAccountChangeEmailScreen(navController)
44 | }
45 | }
46 |
47 | composable {
48 | DefaultScaffold(
49 | topBar = { TopBarWithBack(title = R.string.ChangePassword, navController) }
50 | ) {
51 | SettingsAccountChangePasswordScreen(navController)
52 | }
53 | }
54 |
55 | composable {
56 | DefaultScaffold(
57 | topBar = { TopBarWithBack(title = R.string.DeleteAccount, navController) }
58 | ) {
59 | SettingsAccountDeleteAccountScreen(navController)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/database-logic/src/main/java/dev/medzik/librepass/android/database/datastore/VaultTimeout.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.database.datastore
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.core.Serializer
6 | import androidx.datastore.dataStore
7 | import kotlinx.coroutines.flow.first
8 | import kotlinx.coroutines.runBlocking
9 | import kotlinx.serialization.ExperimentalSerializationApi
10 | import kotlinx.serialization.Serializable
11 | import kotlinx.serialization.json.Json
12 | import kotlinx.serialization.json.decodeFromStream
13 | import kotlinx.serialization.json.encodeToStream
14 | import java.io.InputStream
15 | import java.io.OutputStream
16 |
17 | @Serializable
18 | data class VaultTimeout(
19 | val timeout: VaultTimeoutValue = VaultTimeoutValue.FIFTEEN_MINUTES,
20 | val expires: Long = System.currentTimeMillis()
21 | )
22 |
23 | enum class VaultTimeoutValue(val minutes: Int) {
24 | INSTANT(0),
25 | ONE_MINUTE(1),
26 | FIVE_MINUTES(5),
27 | FIFTEEN_MINUTES(15),
28 | THIRTY_MINUTES(30),
29 | ONE_HOUR(60),
30 | NEVER(-1);
31 |
32 | companion object {
33 | fun fromMinutes(minutes: Int): VaultTimeoutValue {
34 | for (value in entries) {
35 | if (value.minutes == minutes) {
36 | return value
37 | }
38 | }
39 |
40 | throw IllegalArgumentException()
41 | }
42 | }
43 | }
44 |
45 | private object VaultTimeoutSerializer : Serializer {
46 | override val defaultValue = VaultTimeout()
47 |
48 | @OptIn(ExperimentalSerializationApi::class)
49 | override suspend fun readFrom(input: InputStream): VaultTimeout {
50 | return Json.decodeFromStream(input)
51 | }
52 |
53 | @OptIn(ExperimentalSerializationApi::class)
54 | override suspend fun writeTo(
55 | t: VaultTimeout,
56 | output: OutputStream
57 | ) = Json.encodeToStream(t, output)
58 | }
59 |
60 | private val Context.vaultTimeoutDataStore: DataStore by dataStore(
61 | fileName = "vaultTimeout.pb",
62 | serializer = VaultTimeoutSerializer
63 | )
64 |
65 | fun readVaultTimeout(context: Context): VaultTimeout {
66 | return runBlocking { context.vaultTimeoutDataStore.data.first() }
67 | }
68 |
69 | suspend fun writeVaultTimeout(context: Context, preference: VaultTimeout) {
70 | context.vaultTimeoutDataStore.updateData { preference }
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/res/values-vi/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - %d giờ
5 |
6 | Thêm mật mã mới
7 | Thêm máy chủ
8 | Bí mật hai yếu tố
9 | Hủy bỏ
10 | Thiết lập
11 | Sử dụng mật khẩu
12 | Số thẻ
13 | Ghi chú an toàn
14 | Mật khẩu quá ngắn
15 | Lỗi không rõ
16 | Thông tin thêm về đăng nhập
17 | Chào mừng đến với LibrePass
18 | Đăng ký
19 | Dữ liệu đăng nhập
20 | Đăng nhập
21 | Đăng xuất
22 |
23 | - %d phút
24 |
25 | Thêm
26 | Thêm trường
27 | Xác thực hai yếu tố
28 | Bật xác thực sinh trắc học
29 | Vui lòng xác thực chính bạn
30 | Mở khóa
31 | Tên chủ thẻ
32 | Đổi mật khẩu
33 | Dữ liệu thẻ
34 | Xác nhận mật khẩu mới
35 | Địa chỉ e-mail không hợp lệ
36 | Xác nhận mật khẩu
37 | Xóa bỏ
38 | Xóa tài khoản
39 | Biên tập
40 | Lỗi mã hóa/giải mã
41 | Thông tin không hợp lệ
42 | Mật khẩu không đúng
43 | Mật khẩu không khớp
44 | Tháng hết hạn
45 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/utils/Exception.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.utils
2 |
3 | import android.content.Context
4 | import dev.medzik.android.utils.runOnUiThread
5 | import dev.medzik.android.utils.showToast
6 | import dev.medzik.librepass.android.BuildConfig
7 | import dev.medzik.librepass.android.R
8 | import dev.medzik.librepass.client.errors.ApiException
9 | import dev.medzik.librepass.client.errors.ClientException
10 | import dev.medzik.librepass.errors.ServerError
11 | import kotlinx.coroutines.CancellationException
12 |
13 | /** Log exception if debugging is enabled. */
14 | fun Exception.debugLog() {
15 | if (BuildConfig.DEBUG) {
16 | printStackTrace()
17 | }
18 | }
19 |
20 | /** Handle exceptions. Show toast with an error message. */
21 | fun Exception.showErrorToast(context: Context) {
22 | // ignore when job was cancelled (e.g. when left composable)
23 | if (this is CancellationException) return
24 |
25 | // log exception trace if debugging is enabled
26 | debugLog()
27 |
28 | val message = when (this) {
29 | // // handle encrypt exception
30 | // is EncryptException -> { context.getString(R.string.Error_EncryptionError) }
31 | // ignore network error
32 | is ClientException -> {
33 | return
34 | }
35 | // handle api exceptions
36 | is ApiException -> {
37 | getTranslatedErrorMessage(context)
38 | }
39 | // handle other exceptions
40 | else -> {
41 | context.getString(R.string.Error_UnknownError)
42 | }
43 | }
44 |
45 | runOnUiThread { context.showToast(message) }
46 | }
47 |
48 | fun ApiException.getTranslatedErrorMessage(context: Context): String {
49 | return when (getServerError()) {
50 | // ServerError.CipherNotFound -> context.getString(R.string.CipherNotFound)
51 | // ServerError.CollectionNotFound -> context.getString(R.string.CollectionNotFound)
52 | ServerError.Database -> context.getString(R.string.Database)
53 | ServerError.Duplicated -> context.getString(R.string.Duplicated)
54 | ServerError.EmailNotVerified -> context.getString(R.string.EmailNotVerified)
55 | ServerError.InvalidBody -> context.getString(R.string.InvalidBody)
56 | ServerError.InvalidSharedSecret -> context.getString(R.string.InvalidCredentials)
57 | ServerError.InvalidToken -> context.getString(R.string.InvalidToken)
58 | // ServerError.InvalidTwoFactor -> context.getString(R.string.InvalidTwoFactor)
59 | // ServerError.NotFound -> context.getString(R.string.NotFound)
60 | ServerError.RateLimit -> context.getString(R.string.RateLimit)
61 |
62 | else -> message
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 |
16 | - name: Set up JDK 17
17 | uses: actions/setup-java@v4
18 | with:
19 | java-version: '17'
20 | distribution: 'temurin'
21 | cache: gradle
22 |
23 | - name: Setup Android SDK
24 | uses: android-actions/setup-android@v3
25 |
26 | - name: Setup build tool version variable
27 | shell: bash
28 | run: |
29 | BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
30 | echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
31 | echo Last build tool version is: $BUILD_TOOL_VERSION
32 |
33 | - name: Build with Gradle
34 | run: ./gradlew build
35 |
36 | - name: Sign APK
37 | id: sign_apk
38 | uses: r0adkll/sign-android-release@v1
39 | with:
40 | releaseDirectory: app/build/outputs/apk/release
41 | signingKeyBase64: ${{ secrets.SIGNING_KEY }}
42 | alias: ${{ secrets.ALIAS }}
43 | keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
44 | keyPassword: ${{ secrets.KEY_PASSWORD }}
45 | env:
46 | BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
47 |
48 | - name: Prepare release
49 | run: |
50 | mkdir release
51 | cp ${{ steps.sign_apk.outputs.signedReleaseFile }} release/LibrePass-signed.apk
52 |
53 | - name: Upload APK
54 | uses: actions/upload-artifact@v4
55 | with:
56 | name: apk
57 | path: release/*
58 |
59 | release:
60 | runs-on: ubuntu-latest
61 | needs: build
62 |
63 | steps:
64 | - name: Checkout
65 | uses: actions/checkout@v4
66 |
67 | - name: Get changelog
68 | run: |
69 | {
70 | echo 'CHANGELOG<> "$GITHUB_ENV"
74 |
75 | - name: Download APK from build
76 | uses: actions/download-artifact@v4
77 | with:
78 | name: apk
79 | path: apk
80 |
81 | - name: Create release
82 | uses: softprops/action-gh-release@v2
83 | if: github.event.inputs.isMock != 'mock'
84 | with:
85 | token: ${{ secrets.GITHUB_TOKEN }}
86 | prerelease: false
87 | tag_name: ${{ github.ref }}
88 | fail_on_unmatched_files: true
89 | name: ${{ github.ref_name }}
90 | body: ${{ env.CHANGELOG }}
91 | files: apk/*
92 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsAccount.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.settings
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.automirrored.filled.Logout
5 | import androidx.compose.material.icons.filled.Email
6 | import androidx.compose.material.icons.filled.LockReset
7 | import androidx.compose.material.icons.filled.NoAccounts
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.hilt.navigation.compose.hiltViewModel
11 | import androidx.navigation.NavController
12 | import androidx.navigation.NavGraph.Companion.findStartDestination
13 | import dev.medzik.android.compose.ui.IconBox
14 | import dev.medzik.android.compose.ui.preference.BasicPreference
15 | import dev.medzik.librepass.android.R
16 | import dev.medzik.librepass.android.common.LibrePassViewModel
17 | import dev.medzik.librepass.android.ui.screens.Welcome
18 | import dev.medzik.librepass.android.ui.screens.settings.account.SettingsAccountChangeEmail
19 | import dev.medzik.librepass.android.ui.screens.settings.account.SettingsAccountChangePassword
20 | import dev.medzik.librepass.android.ui.screens.settings.account.SettingsAccountDeleteAccount
21 | import kotlinx.coroutines.runBlocking
22 | import kotlinx.serialization.Serializable
23 |
24 | @Serializable
25 | object SettingsAccount
26 |
27 | @Composable
28 | fun SettingsAccountScreen(
29 | navController: NavController,
30 | viewModel: LibrePassViewModel = hiltViewModel()
31 | ) {
32 | BasicPreference(
33 | title = stringResource(R.string.ChangeEmail),
34 | leading = { IconBox(Icons.Default.Email) },
35 | onClick = { navController.navigate(SettingsAccountChangeEmail) }
36 | )
37 |
38 | BasicPreference(
39 | title = stringResource(R.string.ChangePassword),
40 | leading = { IconBox(Icons.Default.LockReset) },
41 | onClick = { navController.navigate(SettingsAccountChangePassword) }
42 | )
43 |
44 | BasicPreference(
45 | title = stringResource(R.string.Logout),
46 | leading = { IconBox(Icons.AutoMirrored.Filled.Logout) },
47 | onClick = {
48 | runBlocking {
49 | val credentials = viewModel.credentialRepository.get()!!
50 |
51 | viewModel.credentialRepository.drop()
52 | viewModel.cipherRepository.drop(credentials.userId)
53 |
54 | navController.navigate(
55 | Welcome
56 | ) {
57 | popUpTo(navController.graph.findStartDestination().id) {
58 | saveState = false
59 | inclusive = true
60 | }
61 | }
62 | }
63 | }
64 | )
65 |
66 | BasicPreference(
67 | title = stringResource(R.string.DeleteAccount),
68 | leading = { IconBox(Icons.Default.NoAccounts) },
69 | onClick = { navController.navigate(SettingsAccountDeleteAccount) }
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/utils/Biometric.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.utils
2 |
3 | import android.content.Context
4 | import androidx.biometric.BiometricManager
5 | import androidx.biometric.BiometricPrompt
6 | import androidx.fragment.app.FragmentActivity
7 | import dev.medzik.librepass.android.R
8 | import javax.crypto.Cipher
9 |
10 | fun showBiometricPromptForUnlock(
11 | context: FragmentActivity,
12 | cipher: Cipher,
13 | onAuthenticationSucceeded: (Cipher) -> Unit,
14 | onAuthenticationFailed: () -> Unit
15 | ) {
16 | val promptInfo =
17 | BiometricPrompt.PromptInfo.Builder()
18 | .setTitle(context.getString(R.string.BiometricUnlock_Title))
19 | .setSubtitle(context.getString(R.string.BiometricUnlock_Subtitle))
20 | .setNegativeButtonText(context.getString(R.string.BiometricUnlock_Button_UsePassword))
21 | .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
22 | .build()
23 |
24 | showBiometricPrompt(context, promptInfo, cipher, onAuthenticationSucceeded, onAuthenticationFailed)
25 | }
26 |
27 | fun showBiometricPromptForSetup(
28 | context: FragmentActivity,
29 | cipher: Cipher,
30 | onAuthenticationSucceeded: (Cipher) -> Unit,
31 | onAuthenticationFailed: () -> Unit
32 | ) {
33 | val promptInfo = BiometricPrompt.PromptInfo.Builder()
34 | .setTitle(context.getString(R.string.BiometricSetup_Title))
35 | .setSubtitle(context.getString(R.string.BiometricSetup_Subtitle))
36 | .setNegativeButtonText(context.getString(R.string.BiometricSetup_Button_Cancel))
37 | .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
38 | .build()
39 |
40 | showBiometricPrompt(context, promptInfo, cipher, onAuthenticationSucceeded, onAuthenticationFailed)
41 | }
42 |
43 | private fun showBiometricPrompt(
44 | context: FragmentActivity,
45 | promptInfo: BiometricPrompt.PromptInfo,
46 | cipher: Cipher,
47 | onAuthenticationSucceeded: (Cipher) -> Unit,
48 | onAuthenticationFailed: () -> Unit
49 | ) {
50 | val biometricPrompt = BiometricPrompt(
51 | context,
52 | object : BiometricPrompt.AuthenticationCallback() {
53 | override fun onAuthenticationError(
54 | errorCode: Int,
55 | errString: CharSequence
56 | ) = onAuthenticationFailed()
57 |
58 | override fun onAuthenticationSucceeded(
59 | result: BiometricPrompt.AuthenticationResult
60 | ) = onAuthenticationSucceeded(result.cryptoObject?.cipher!!)
61 |
62 | override fun onAuthenticationFailed() = onAuthenticationFailed()
63 | }
64 | )
65 |
66 | biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
67 | }
68 |
69 | fun checkIfBiometricAvailable(context: Context): Boolean {
70 | val status = BiometricManager.from(context)
71 | .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
72 |
73 | // return true when available
74 | return status == BiometricManager.BIOMETRIC_SUCCESS
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/Welcome.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.material3.Button
11 | import androidx.compose.material3.OutlinedButton
12 | import androidx.compose.material3.Scaffold
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.unit.dp
20 | import androidx.navigation.NavController
21 | import com.google.accompanist.drawablepainter.DrawablePainter
22 | import dev.medzik.android.compose.ui.TopAppBarMultiColor
23 | import dev.medzik.librepass.android.R
24 | import dev.medzik.librepass.android.ui.screens.auth.Login
25 | import dev.medzik.librepass.android.ui.screens.auth.Register
26 | import kotlinx.serialization.Serializable
27 |
28 | @Serializable
29 | object Welcome
30 |
31 | @Composable
32 | fun WelcomeScreen(navController: NavController) {
33 | val context = LocalContext.current
34 |
35 | // get app icon
36 | val icon = context.packageManager.getApplicationIcon(context.packageName)
37 |
38 | Scaffold(
39 | topBar = { TopAppBarMultiColor(firstText = "Libre", secondText = "Pass") }
40 | ) { innerPadding ->
41 | Column(
42 | modifier = Modifier
43 | .fillMaxSize()
44 | .padding(innerPadding),
45 | horizontalAlignment = Alignment.CenterHorizontally,
46 | verticalArrangement = Arrangement.Center
47 | ) {
48 | Image(
49 | painter = DrawablePainter(icon),
50 | contentDescription = null,
51 | modifier = Modifier.size(128.dp)
52 | )
53 |
54 | Text(
55 | text = stringResource(R.string.WelcomeScreen_Title),
56 | modifier = Modifier.padding(top = 20.dp)
57 | )
58 |
59 | Button(
60 | onClick = { navController.navigate(Register) },
61 | modifier = Modifier
62 | .fillMaxWidth()
63 | .padding(horizontal = 90.dp)
64 | .padding(top = 20.dp)
65 | ) {
66 | Text(stringResource(R.string.Register))
67 | }
68 |
69 | OutlinedButton(
70 | onClick = { navController.navigate(Login) },
71 | modifier = Modifier
72 | .fillMaxWidth()
73 | .padding(horizontal = 90.dp)
74 | .padding(top = 8.dp)
75 | ) {
76 | Text(stringResource(R.string.Login))
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/components/auth/ChoiceServer.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.components.auth
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.MutableState
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.unit.dp
15 | import androidx.navigation.NavController
16 | import dev.medzik.android.compose.ui.dialog.PickerDialog
17 | import dev.medzik.android.compose.ui.dialog.rememberDialogState
18 | import dev.medzik.librepass.android.R
19 | import dev.medzik.librepass.android.database.datastore.CustomServers
20 | import dev.medzik.librepass.android.database.datastore.readCustomServers
21 | import dev.medzik.librepass.android.ui.screens.auth.AddCustomServer
22 | import dev.medzik.librepass.client.Server
23 | import kotlinx.coroutines.runBlocking
24 |
25 | @Composable
26 | fun ChoiceServer(navController: NavController, server: MutableState) {
27 | val serverChoiceDialog = rememberDialogState()
28 |
29 | @Composable
30 | fun getServerName(server: String): String {
31 | return when (server) {
32 | Server.PRODUCTION -> {
33 | stringResource(R.string.Server_Official)
34 | }
35 | else -> server
36 | }
37 | }
38 |
39 | Row(
40 | modifier = Modifier
41 | .padding(vertical = 8.dp)
42 | .clickable { serverChoiceDialog.show() }
43 | ) {
44 | Text(
45 | text = stringResource(R.string.ServerAddress) + ": ",
46 | style = MaterialTheme.typography.bodyMedium,
47 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
48 | )
49 |
50 | Text(
51 | text = getServerName(server.value),
52 | style = MaterialTheme.typography.bodyMedium,
53 | color = MaterialTheme.colorScheme.primary
54 | )
55 | }
56 |
57 | val context = LocalContext.current
58 |
59 | val servers = listOf(
60 | CustomServers(
61 | name = stringResource(R.string.Server_Official),
62 | address = Server.PRODUCTION
63 | )
64 | )
65 | .plus(runBlocking { readCustomServers(context) })
66 | .plus(CustomServers(stringResource(R.string.Server_Choice_Dialog_AddCustom), "custom_server"))
67 |
68 | PickerDialog(
69 | state = serverChoiceDialog,
70 | title = stringResource(R.string.ServerAddress),
71 | items = servers,
72 | onSelected = {
73 | if (it.address == "custom_server") {
74 | navController.navigate(AddCustomServer)
75 | } else {
76 | server.value = it.address
77 | }
78 | }
79 | ) {
80 | Text(
81 | text = it.name,
82 | modifier = Modifier
83 | .padding(vertical = 12.dp)
84 | .fillMaxWidth()
85 | )
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val md_theme_light_primary = Color(0xFF9B4600)
6 | val md_theme_light_onPrimary = Color(0xFFFFFFFF)
7 | val md_theme_light_primaryContainer = Color(0xFFFFDBC9)
8 | val md_theme_light_onPrimaryContainer = Color(0xFF331200)
9 | val md_theme_light_secondary = Color(0xFF765847)
10 | val md_theme_light_onSecondary = Color(0xFFFFFFFF)
11 | val md_theme_light_secondaryContainer = Color(0xFFFFDBC9)
12 | val md_theme_light_onSecondaryContainer = Color(0xFF2B160A)
13 | val md_theme_light_tertiary = Color(0xFF626032)
14 | val md_theme_light_onTertiary = Color(0xFFFFFFFF)
15 | val md_theme_light_tertiaryContainer = Color(0xFFE9E5AB)
16 | val md_theme_light_onTertiaryContainer = Color(0xFF1E1C00)
17 | val md_theme_light_error = Color(0xFFBA1A1A)
18 | val md_theme_light_errorContainer = Color(0xFFFFDAD6)
19 | val md_theme_light_onError = Color(0xFFFFFFFF)
20 | val md_theme_light_onErrorContainer = Color(0xFF410002)
21 | val md_theme_light_background = Color(0xFFFFFBFF)
22 | val md_theme_light_onBackground = Color(0xFF201A17)
23 | val md_theme_light_surface = Color(0xFFFFFBFF)
24 | val md_theme_light_onSurface = Color(0xFF201A17)
25 | val md_theme_light_surfaceVariant = Color(0xFFF4DED4)
26 | val md_theme_light_onSurfaceVariant = Color(0xFF52443C)
27 | val md_theme_light_outline = Color(0xFF85746B)
28 | val md_theme_light_inverseOnSurface = Color(0xFFFBEEE9)
29 | val md_theme_light_inverseSurface = Color(0xFF362F2C)
30 | val md_theme_light_inversePrimary = Color(0xFFFFB68D)
31 | val md_theme_light_surfaceTint = Color(0xFF9B4600)
32 | val md_theme_light_outlineVariant = Color(0xFFD7C2B8)
33 | val md_theme_light_scrim = Color(0xFF000000)
34 |
35 | val md_theme_dark_primary = Color(0xFFFFB68D)
36 | val md_theme_dark_onPrimary = Color(0xFF532200)
37 | val md_theme_dark_primaryContainer = Color(0xFF763300)
38 | val md_theme_dark_onPrimaryContainer = Color(0xFFFFDBC9)
39 | val md_theme_dark_secondary = Color(0xFFE5BEAA)
40 | val md_theme_dark_onSecondary = Color(0xFF432B1D)
41 | val md_theme_dark_secondaryContainer = Color(0xFF5C4131)
42 | val md_theme_dark_onSecondaryContainer = Color(0xFFFFDBC9)
43 | val md_theme_dark_tertiary = Color(0xFFCDC991)
44 | val md_theme_dark_onTertiary = Color(0xFF333208)
45 | val md_theme_dark_tertiaryContainer = Color(0xFF4A481D)
46 | val md_theme_dark_onTertiaryContainer = Color(0xFFE9E5AB)
47 | val md_theme_dark_error = Color(0xFFFFB4AB)
48 | val md_theme_dark_errorContainer = Color(0xFF93000A)
49 | val md_theme_dark_onError = Color(0xFF690005)
50 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
51 | val md_theme_dark_background = Color(0xFF201A17)
52 | val md_theme_dark_onBackground = Color(0xFFECE0DB)
53 | val md_theme_dark_surface = Color(0xFF201A17)
54 | val md_theme_dark_onSurface = Color(0xFFECE0DB)
55 | val md_theme_dark_surfaceVariant = Color(0xFF52443C)
56 | val md_theme_dark_onSurfaceVariant = Color(0xFFD7C2B8)
57 | val md_theme_dark_outline = Color(0xFF9F8D84)
58 | val md_theme_dark_inverseOnSurface = Color(0xFF201A17)
59 | val md_theme_dark_inverseSurface = Color(0xFFECE0DB)
60 | val md_theme_dark_inversePrimary = Color(0xFF9B4600)
61 | val md_theme_dark_surfaceTint = Color(0xFFFFB68D)
62 | val md_theme_dark_outlineVariant = Color(0xFF52443C)
63 | val md_theme_dark_scrim = Color(0xFF000000)
64 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/DeleteAccount.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.settings.account
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.rememberCoroutineScope
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.platform.LocalContext
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.text.input.KeyboardType
14 | import androidx.compose.ui.unit.dp
15 | import androidx.hilt.navigation.compose.hiltViewModel
16 | import androidx.navigation.NavController
17 | import dev.medzik.android.compose.rememberMutable
18 | import dev.medzik.android.compose.ui.LoadingButton
19 | import dev.medzik.android.utils.showToast
20 | import dev.medzik.librepass.android.R
21 | import dev.medzik.librepass.android.common.LibrePassViewModel
22 | import dev.medzik.librepass.android.common.haveNetworkConnection
23 | import dev.medzik.librepass.android.ui.components.TextInputField
24 | import dev.medzik.librepass.android.utils.showErrorToast
25 | import dev.medzik.librepass.client.Server
26 | import dev.medzik.librepass.client.api.UserClient
27 | import kotlinx.coroutines.Dispatchers
28 | import kotlinx.coroutines.launch
29 | import kotlinx.serialization.Serializable
30 |
31 | @Serializable
32 | object SettingsAccountDeleteAccount
33 |
34 | @Composable
35 | fun SettingsAccountDeleteAccountScreen(
36 | navController: NavController,
37 | viewModel: LibrePassViewModel = hiltViewModel()
38 | ) {
39 | val context = LocalContext.current
40 | val credentials = viewModel.credentialRepository.get() ?: return
41 |
42 | var loading by rememberMutable(false)
43 | var password by rememberMutable("")
44 | val scope = rememberCoroutineScope()
45 |
46 | val userClient = UserClient(
47 | email = credentials.email,
48 | apiKey = credentials.apiKey,
49 | apiUrl = credentials.apiUrl ?: Server.PRODUCTION
50 | )
51 |
52 | fun deleteAccount(password: String) {
53 | if (!context.haveNetworkConnection()) {
54 | context.showToast(R.string.Error_NoInternetConnection)
55 | return
56 | }
57 |
58 | loading = true
59 |
60 | scope.launch(Dispatchers.IO) {
61 | try {
62 | userClient.deleteAccount(password)
63 |
64 | navigateToWelcomeAndLogout(viewModel, navController, credentials.userId)
65 | } catch (e: Exception) {
66 | e.showErrorToast(context)
67 |
68 | loading = false
69 | }
70 | }
71 | }
72 |
73 | TextInputField(
74 | label = stringResource(R.string.Password),
75 | value = password,
76 | onValueChange = { password = it },
77 | hidden = true,
78 | keyboardType = KeyboardType.Password
79 | )
80 |
81 | LoadingButton(
82 | loading = loading,
83 | onClick = { deleteAccount(password) },
84 | enabled = password.isNotEmpty(),
85 | modifier = Modifier
86 | .fillMaxWidth()
87 | .padding(horizontal = 40.dp, vertical = 8.dp)
88 | ) {
89 | Text(stringResource(R.string.DeleteAccount))
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.dagger.hilt)
4 | alias(libs.plugins.kotlin.ksp)
5 | alias(libs.plugins.kotlin.android)
6 | alias(libs.plugins.kotlin.serialization)
7 | alias(libs.plugins.kotlin.parcelize)
8 | alias(libs.plugins.compose.compiler)
9 | }
10 |
11 | android {
12 | namespace = "dev.medzik.librepass.android"
13 | compileSdk = libs.versions.android.sdk.compile.get().toInt()
14 |
15 | defaultConfig {
16 | applicationId = "dev.medzik.librepass.android"
17 | minSdk = libs.versions.android.sdk.min.get().toInt()
18 | targetSdk = libs.versions.android.sdk.target.get().toInt()
19 | versionCode = 18
20 | versionName = "1.3.1"
21 |
22 | vectorDrawables {
23 | useSupportLibrary = true
24 | }
25 | }
26 |
27 | buildTypes {
28 | release {
29 | isMinifyEnabled = true
30 | proguardFiles(
31 | getDefaultProguardFile("proguard-android-optimize.txt"),
32 | "proguard-rules.pro"
33 | )
34 | }
35 |
36 | debug {
37 | applicationIdSuffix = ".debug"
38 | versionNameSuffix = "-debug"
39 | }
40 | }
41 |
42 | androidResources {
43 | @Suppress("UnstableApiUsage")
44 | generateLocaleConfig = true
45 | }
46 |
47 | lint {
48 | warning.add("MissingTranslation")
49 | }
50 |
51 | compileOptions {
52 | sourceCompatibility = JavaVersion.VERSION_17
53 | targetCompatibility = JavaVersion.VERSION_17
54 | }
55 |
56 | kotlinOptions {
57 | jvmTarget = JavaVersion.VERSION_17.toString()
58 | }
59 |
60 | buildFeatures {
61 | compose = true
62 | viewBinding = true
63 | buildConfig = true
64 | }
65 |
66 | packaging {
67 | resources {
68 | excludes += "/META-INF/*"
69 | }
70 | }
71 | }
72 |
73 | dependencies {
74 | implementation(libs.androidx.core.ktx)
75 | implementation(libs.androidx.activity.compose)
76 |
77 | implementation(libs.compose.material.icons)
78 | implementation(libs.compose.material3)
79 | implementation(libs.compose.navigation)
80 | implementation(libs.compose.ui)
81 |
82 | implementation(libs.accompanist.drawablepainter)
83 |
84 | implementation(libs.androidx.biometric.ktx)
85 |
86 | // used for calling `onResume` and locking vault after X minutes
87 | implementation(libs.compose.lifecycle.runtime)
88 |
89 | implementation(projects.databaseLogic)
90 |
91 | // dagger
92 | implementation(libs.dagger.hilt)
93 | implementation(libs.hilt.navigation.compose)
94 | ksp(libs.dagger.hilt.compiler)
95 |
96 | implementation(libs.coil.compose)
97 |
98 | implementation(libs.librepass.client)
99 | implementation(libs.otp)
100 |
101 | implementation(libs.kotlinx.coroutines)
102 | implementation(libs.kotlinx.serialization.json)
103 |
104 | implementation(projects.common)
105 | implementation(projects.businessLogic)
106 |
107 | // for splash screen with material3 and dynamic color
108 | implementation(libs.google.material)
109 |
110 | implementation(libs.zxing.android) { isTransitive = false }
111 | implementation(libs.zxing)
112 |
113 | implementation(libs.medzik.android.compose)
114 | implementation(libs.medzik.android.crypto)
115 | implementation(libs.medzik.android.utils)
116 |
117 | // for testing
118 | debugImplementation(libs.compose.ui.test.manifest)
119 |
120 | // for preview support
121 | debugImplementation(libs.compose.ui.tooling)
122 | implementation(libs.compose.ui.tooling.preview)
123 | }
124 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangeEmail.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.settings.account
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.rememberCoroutineScope
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.platform.LocalContext
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.text.input.KeyboardType
14 | import androidx.compose.ui.unit.dp
15 | import androidx.hilt.navigation.compose.hiltViewModel
16 | import androidx.navigation.NavController
17 | import dev.medzik.android.compose.rememberMutable
18 | import dev.medzik.android.compose.ui.LoadingButton
19 | import dev.medzik.android.utils.showToast
20 | import dev.medzik.librepass.android.R
21 | import dev.medzik.librepass.android.common.LibrePassViewModel
22 | import dev.medzik.librepass.android.common.haveNetworkConnection
23 | import dev.medzik.librepass.android.ui.components.TextInputField
24 | import dev.medzik.librepass.android.utils.showErrorToast
25 | import dev.medzik.librepass.client.Server
26 | import dev.medzik.librepass.client.api.UserClient
27 | import kotlinx.coroutines.Dispatchers
28 | import kotlinx.coroutines.launch
29 | import kotlinx.serialization.Serializable
30 |
31 | @Serializable
32 | object SettingsAccountChangeEmail
33 |
34 | @Composable
35 | fun SettingsAccountChangeEmailScreen(
36 | navController: NavController,
37 | viewModel: LibrePassViewModel = hiltViewModel()
38 | ) {
39 | val context = LocalContext.current
40 | val credentials = viewModel.credentialRepository.get() ?: return
41 |
42 | var newEmail by rememberMutable("")
43 | var password by rememberMutable("")
44 | var loading by rememberMutable(false)
45 |
46 | val scope = rememberCoroutineScope()
47 |
48 | val userClient = UserClient(
49 | email = credentials.email,
50 | apiKey = credentials.apiKey,
51 | apiUrl = credentials.apiUrl ?: Server.PRODUCTION
52 | )
53 |
54 | fun changeEmail(newEmail: String, password: String) {
55 | if (!context.haveNetworkConnection()) {
56 | context.showToast(R.string.Error_NoInternetConnection)
57 | return
58 | }
59 |
60 | loading = true
61 |
62 | // TODO: show error "invalid password"
63 |
64 | scope.launch(Dispatchers.IO) {
65 | try {
66 | userClient.changeEmail(newEmail, password)
67 |
68 | navigateToWelcomeAndLogout(viewModel, navController, credentials.userId)
69 | } catch (e: Exception) {
70 | e.showErrorToast(context)
71 |
72 | loading = false
73 | }
74 | }
75 | }
76 |
77 | TextInputField(
78 | label = stringResource(R.string.NewEmail),
79 | value = newEmail,
80 | onValueChange = { newEmail = it },
81 | isError = newEmail.isNotEmpty() && !newEmail.contains('@'),
82 | errorMessage = stringResource(R.string.Error_InvalidEmail),
83 | keyboardType = KeyboardType.Email
84 | )
85 |
86 | TextInputField(
87 | label = stringResource(R.string.Password),
88 | value = password,
89 | onValueChange = { password = it },
90 | emptySupportingText = true,
91 | hidden = true,
92 | keyboardType = KeyboardType.Password,
93 | )
94 |
95 | LoadingButton(
96 | loading = loading,
97 | onClick = { changeEmail(newEmail, password) },
98 | enabled = newEmail.isNotEmpty() && newEmail.contains('@') && password.isNotEmpty(),
99 | modifier = Modifier
100 | .fillMaxWidth()
101 | .padding(horizontal = 40.dp, vertical = 8.dp)
102 | ) {
103 | Text(stringResource(R.string.ChangeEmail))
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.compose.foundation.isSystemInDarkTheme
8 | import androidx.compose.material3.AlertDialog
9 | import androidx.compose.material3.Text
10 | import androidx.compose.material3.TextButton
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.setValue
15 | import androidx.fragment.app.FragmentActivity
16 | import androidx.navigation.NavController
17 | import dagger.hilt.android.AndroidEntryPoint
18 | import dev.medzik.android.utils.openEmailApplication
19 | import dev.medzik.android.utils.showToast
20 | import dev.medzik.librepass.android.business.VaultCache
21 | import dev.medzik.librepass.android.common.popUpToDestination
22 | import dev.medzik.librepass.android.database.Repository
23 | import dev.medzik.librepass.android.ui.LibrePassNavigation
24 | import dev.medzik.librepass.android.ui.screens.auth.Unlock
25 | import dev.medzik.librepass.android.ui.theme.LibrePassTheme
26 | import javax.inject.Inject
27 |
28 | @AndroidEntryPoint
29 | class MainActivity : FragmentActivity() {
30 | @Inject
31 | lateinit var repository: Repository
32 |
33 | @Inject
34 | lateinit var vault: VaultCache
35 |
36 | override fun onCreate(savedInstanceState: Bundle?) {
37 | super.onCreate(savedInstanceState)
38 |
39 | enableEdgeToEdge()
40 |
41 | // handle uncaught exceptions
42 | Thread.setDefaultUncaughtExceptionHandler { _, e ->
43 | Log.e("LibrePass", "Uncaught exception", e)
44 |
45 | openEmailApplication(
46 | email = "contact@librepass.org",
47 | subject = "[Bug] [Android]: ",
48 | body = "\n\n\n---- Stack trace for debugging ----\n\n${Log.getStackTraceString(e)}"
49 | )
50 |
51 | finish()
52 | }
53 |
54 | MigrationsManager.run(this, repository)
55 |
56 | // retrieves aes key for vault decryption if key is valid
57 | vault.getSecretsIfNotExpired(this)
58 |
59 | this.showToast("This application is deprecated.")
60 |
61 | setContent {
62 | LibrePassTheme(
63 | darkTheme = isSystemInDarkTheme(),
64 | dynamicColor = true
65 | ) {
66 | var showDeprecatedWarning by remember { mutableStateOf(true) }
67 | if (showDeprecatedWarning) {
68 | AlertDialog(
69 | onDismissRequest = {
70 | showDeprecatedWarning = false
71 | },
72 | confirmButton = {
73 | TextButton(
74 | onClick = {
75 | showDeprecatedWarning = false
76 | }
77 | ) {
78 | Text("OK")
79 | }
80 | },
81 | title = {
82 | Text("Warning")
83 | },
84 | text = {
85 | Text("This application is deprecated. Please migrate all passwords before December 2024")
86 | }
87 | )
88 | }
89 |
90 | LibrePassNavigation()
91 | }
92 | }
93 | }
94 |
95 | override fun onPause() {
96 | super.onPause()
97 |
98 | // check if user is logged
99 | if (repository.credentials.get() == null) return
100 |
101 | vault.saveVaultExpiration(this)
102 | }
103 |
104 | /** Called from [LibrePassNavigation]. */
105 | fun onResume(navController: NavController) {
106 | // check if user is logged
107 | if (repository.credentials.get() == null) return
108 |
109 | val expired = vault.handleExpiration(this)
110 | if (expired) {
111 | navController.navigate(Unlock) {
112 | popUpToDestination(Unlock)
113 | }
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | accompanist = "0.34.0"
3 | agp = "8.5.0"
4 | android-sdk-compile = "34"
5 | android-sdk-min = "24"
6 | android-sdk-target = "34"
7 | androidx-activity = "1.9.0"
8 | androidx-biometric = "1.2.0-alpha05"
9 | androidx-core = "1.13.1"
10 | androidx-datastore = "1.1.1"
11 | androidx-room = "2.6.1"
12 | coil-compose = "2.6.0"
13 | compose = "1.6.8"
14 | compose-lifecycle-runtime = "2.8.2"
15 | compose-material3 = "1.2.1"
16 | compose-navigation = "2.8.0-beta03"
17 | dagger = "2.51.1"
18 | google-material = "1.12.0"
19 | hilt = "1.2.0"
20 | kotlin = "2.0.0"
21 | kotlin-ksp = "2.0.0-1.0.22"
22 | kotlinx-coroutines = "1.8.1"
23 | kotlinx-serialization = "1.7.0"
24 | librepass-client = "1.6.2"
25 | medzik-android-utils = "1.6.1"
26 | otp = "1.0.1"
27 | zxing = "3.5.3"
28 | zxing-android = "4.3.0"
29 |
30 | [libraries]
31 | accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" }
32 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
33 | androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "androidx-biometric" }
34 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
35 | androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "androidx-datastore" }
36 | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" }
37 | androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" }
38 | androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" }
39 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" }
40 | compose-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle-runtime" }
41 | compose-material-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
42 | compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" }
43 | compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navigation" }
44 | compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
45 | compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" }
46 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
47 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
48 | dagger-hilt = { module = "com.google.dagger:hilt-android", version.ref = "dagger" }
49 | dagger-hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "dagger" }
50 | google-material = { module = "com.google.android.material:material", version.ref = "google-material" }
51 | hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt" }
52 | kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
53 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
54 | librepass-client = { module = "dev.medzik.librepass:client", version.ref = "librepass-client" }
55 | medzik-android-compose = { module = "dev.medzik.android:compose", version.ref = "medzik-android-utils" }
56 | medzik-android-crypto = { module = "dev.medzik.android:crypto", version.ref = "medzik-android-utils" }
57 | medzik-android-utils = { module = "dev.medzik.android:utils", version.ref = "medzik-android-utils" }
58 | otp = { module = "dev.medzik:otp", version.ref = "otp" }
59 | zxing = { module = "com.google.zxing:core", version.ref = "zxing" }
60 | zxing-android = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing-android" }
61 |
62 | [plugins]
63 | android-application = { id = "com.android.application", version.ref = "agp" }
64 | android-library = { id = "com.android.library", version.ref = "agp" }
65 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
66 | dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }
67 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
68 | kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" }
69 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
70 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
71 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material3.*
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.platform.LocalContext
9 |
10 | /**
11 | * Default light theme colors when dynamic color is not available.
12 | */
13 | private val lightColorScheme = lightColorScheme(
14 | primary = md_theme_light_primary,
15 | onPrimary = md_theme_light_onPrimary,
16 | primaryContainer = md_theme_light_primaryContainer,
17 | onPrimaryContainer = md_theme_light_onPrimaryContainer,
18 | secondary = md_theme_light_secondary,
19 | onSecondary = md_theme_light_onSecondary,
20 | secondaryContainer = md_theme_light_secondaryContainer,
21 | onSecondaryContainer = md_theme_light_onSecondaryContainer,
22 | tertiary = md_theme_light_tertiary,
23 | onTertiary = md_theme_light_onTertiary,
24 | tertiaryContainer = md_theme_light_tertiaryContainer,
25 | onTertiaryContainer = md_theme_light_onTertiaryContainer,
26 | error = md_theme_light_error,
27 | errorContainer = md_theme_light_errorContainer,
28 | onError = md_theme_light_onError,
29 | onErrorContainer = md_theme_light_onErrorContainer,
30 | background = md_theme_light_background,
31 | onBackground = md_theme_light_onBackground,
32 | surface = md_theme_light_surface,
33 | onSurface = md_theme_light_onSurface,
34 | surfaceVariant = md_theme_light_surfaceVariant,
35 | onSurfaceVariant = md_theme_light_onSurfaceVariant,
36 | outline = md_theme_light_outline,
37 | inverseOnSurface = md_theme_light_inverseOnSurface,
38 | inverseSurface = md_theme_light_inverseSurface,
39 | inversePrimary = md_theme_light_inversePrimary,
40 | surfaceTint = md_theme_light_surfaceTint,
41 | outlineVariant = md_theme_light_outlineVariant,
42 | scrim = md_theme_light_scrim
43 | )
44 |
45 | /**
46 | * Default dark theme colors when dynamic color is not available.
47 | */
48 | private val darkColorScheme = darkColorScheme(
49 | primary = md_theme_dark_primary,
50 | onPrimary = md_theme_dark_onPrimary,
51 | primaryContainer = md_theme_dark_primaryContainer,
52 | onPrimaryContainer = md_theme_dark_onPrimaryContainer,
53 | secondary = md_theme_dark_secondary,
54 | onSecondary = md_theme_dark_onSecondary,
55 | secondaryContainer = md_theme_dark_secondaryContainer,
56 | onSecondaryContainer = md_theme_dark_onSecondaryContainer,
57 | tertiary = md_theme_dark_tertiary,
58 | onTertiary = md_theme_dark_onTertiary,
59 | tertiaryContainer = md_theme_dark_tertiaryContainer,
60 | onTertiaryContainer = md_theme_dark_onTertiaryContainer,
61 | error = md_theme_dark_error,
62 | errorContainer = md_theme_dark_errorContainer,
63 | onError = md_theme_dark_onError,
64 | onErrorContainer = md_theme_dark_onErrorContainer,
65 | background = md_theme_dark_background,
66 | onBackground = md_theme_dark_onBackground,
67 | surface = md_theme_dark_surface,
68 | onSurface = md_theme_dark_onSurface,
69 | surfaceVariant = md_theme_dark_surfaceVariant,
70 | onSurfaceVariant = md_theme_dark_onSurfaceVariant,
71 | outline = md_theme_dark_outline,
72 | inverseOnSurface = md_theme_dark_inverseOnSurface,
73 | inverseSurface = md_theme_dark_inverseSurface,
74 | inversePrimary = md_theme_dark_inversePrimary,
75 | surfaceTint = md_theme_dark_surfaceTint,
76 | outlineVariant = md_theme_dark_outlineVariant,
77 | scrim = md_theme_dark_scrim
78 | )
79 |
80 | @Composable
81 | fun LibrePassTheme(
82 | darkTheme: Boolean,
83 | // Dynamic color is available on Android 12+
84 | dynamicColor: Boolean,
85 | content: @Composable () -> Unit
86 | ) {
87 | val colorScheme = when {
88 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
89 | val context = LocalContext.current
90 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
91 | }
92 |
93 | darkTheme -> darkColorScheme
94 | else -> lightColorScheme
95 | }
96 |
97 | MaterialTheme(
98 | colorScheme = colorScheme,
99 | typography = Typography,
100 | ) {
101 | Surface(
102 | color = MaterialTheme.colorScheme.surface,
103 | modifier = Modifier.fillMaxSize(),
104 | content = content
105 | )
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/auth/AddCustomServer.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.auth
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.text.KeyboardOptions
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Dns
10 | import androidx.compose.material.icons.filled.Draw
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.setValue
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.text.input.KeyboardType
20 | import androidx.compose.ui.unit.dp
21 | import androidx.navigation.NavController
22 | import com.google.gson.JsonSyntaxException
23 | import dev.medzik.android.compose.rememberMutable
24 | import dev.medzik.android.compose.ui.LoadingButton
25 | import dev.medzik.android.compose.ui.textfield.AnimatedTextField
26 | import dev.medzik.android.compose.ui.textfield.TextFieldValue
27 | import dev.medzik.android.utils.runOnIOThread
28 | import dev.medzik.android.utils.runOnUiThread
29 | import dev.medzik.android.utils.showToast
30 | import dev.medzik.librepass.android.R
31 | import dev.medzik.librepass.android.database.datastore.CustomServers
32 | import dev.medzik.librepass.android.database.datastore.readCustomServers
33 | import dev.medzik.librepass.android.database.datastore.writeCustomServers
34 | import dev.medzik.librepass.client.api.checkApiConnection
35 | import kotlinx.serialization.Serializable
36 |
37 | @Serializable
38 | object AddCustomServer
39 |
40 | @Composable
41 | fun AddCustomServerScreen(navController: NavController) {
42 | val context = LocalContext.current
43 |
44 | var loading by rememberMutable(false)
45 | var server by rememberMutable(CustomServers("", "https://"))
46 |
47 | fun submit() {
48 | loading = true
49 |
50 | runOnIOThread {
51 | // TODO: Delete try when released new version of LibrePass client library
52 | try {
53 | if (!checkApiConnection(server.address)) {
54 | context.showToast(R.string.Tost_NoServerConnection)
55 |
56 | loading = false
57 |
58 | return@runOnIOThread
59 | }
60 | } catch (e: JsonSyntaxException) {
61 | context.showToast(R.string.Tost_NoServerConnection)
62 |
63 | loading = false
64 |
65 | return@runOnIOThread
66 | }
67 |
68 | val servers = readCustomServers(context)
69 | writeCustomServers(context, servers.plus(server))
70 |
71 | runOnUiThread { navController.popBackStack() }
72 | }
73 | }
74 |
75 | Column(
76 | verticalArrangement = Arrangement.spacedBy(10.dp)
77 | ) {
78 | AnimatedTextField(
79 | label = stringResource(R.string.Name),
80 | value = TextFieldValue(
81 | value = server.name,
82 | onChange = { server = server.copy(name = it) }
83 | ),
84 | leading = {
85 | Icon(
86 | imageVector = Icons.Default.Draw,
87 | contentDescription = null
88 | )
89 | }
90 | )
91 |
92 | AnimatedTextField(
93 | label = stringResource(R.string.ServerAddress),
94 | value = TextFieldValue(
95 | value = server.address,
96 | onChange = { server = server.copy(address = it) }
97 | ),
98 | keyboardOptions = KeyboardOptions(
99 | keyboardType = KeyboardType.Uri
100 | ),
101 | clearButton = true,
102 | leading = {
103 | Icon(
104 | imageVector = Icons.Default.Dns,
105 | contentDescription = null
106 | )
107 | }
108 | )
109 | }
110 |
111 | LoadingButton(
112 | loading = loading,
113 | onClick = { submit() },
114 | enabled = server.name.isNotEmpty() && server.address.isNotEmpty(),
115 | modifier = Modifier
116 | .fillMaxWidth()
117 | .padding(horizontal = 80.dp, vertical = 8.dp)
118 | ) {
119 | Text(stringResource(R.string.Add))
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/Search.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.vault
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.heightIn
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Search
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.Scaffold
13 | import androidx.compose.material3.TextField
14 | import androidx.compose.material3.TextFieldDefaults
15 | import androidx.compose.material3.TopAppBar
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.getValue
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.runtime.setValue
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.unit.dp
23 | import androidx.hilt.navigation.compose.hiltViewModel
24 | import androidx.navigation.NavController
25 | import dev.medzik.android.compose.icons.TopAppBarBackIcon
26 | import dev.medzik.android.compose.rememberMutable
27 | import dev.medzik.librepass.android.common.LibrePassViewModel
28 | import dev.medzik.librepass.android.ui.components.CipherCard
29 | import dev.medzik.librepass.types.cipher.CipherType
30 | import kotlinx.serialization.Serializable
31 |
32 | @Serializable
33 | object Search
34 |
35 | @OptIn(ExperimentalMaterial3Api::class)
36 | @Composable
37 | fun SearchScreen(
38 | navController: NavController,
39 | viewModel: LibrePassViewModel = hiltViewModel()
40 | ) {
41 | val ciphers = remember { viewModel.vault.getSortedCiphers() }
42 |
43 | var searchText by rememberMutable("")
44 |
45 | Scaffold(
46 | topBar = {
47 | TopAppBar(
48 | title = {
49 | TextField(
50 | value = searchText,
51 | onValueChange = { searchText = it },
52 | leadingIcon = {
53 | Icon(
54 | imageVector = Icons.Default.Search,
55 | contentDescription = null
56 | )
57 | },
58 | colors = TextFieldDefaults.colors(
59 | focusedContainerColor = Color.Transparent,
60 | unfocusedContainerColor = Color.Transparent,
61 | focusedIndicatorColor = Color.Transparent,
62 | unfocusedIndicatorColor = Color.Transparent
63 | ),
64 | modifier = Modifier
65 | .fillMaxWidth()
66 | .heightIn(min = 56.dp)
67 | )
68 | },
69 | navigationIcon = { TopAppBarBackIcon(navController) }
70 | )
71 | }
72 | ) { innerPadding ->
73 | LazyColumn(
74 | modifier = Modifier
75 | .fillMaxSize()
76 | .padding(innerPadding)
77 | .padding(horizontal = 16.dp)
78 | ) {
79 | val filteredCiphers = ciphers.filter {
80 | when (it.type) {
81 | CipherType.Login -> {
82 | it.loginData!!.name.lowercase().contains(searchText) ||
83 | it.loginData!!.username?.lowercase()?.contains(searchText) ?: false
84 | }
85 |
86 | CipherType.SecureNote -> {
87 | it.secureNoteData!!.title.lowercase().contains(searchText)
88 | }
89 |
90 | CipherType.Card -> {
91 | it.cardData!!.cardholderName.lowercase().contains(searchText)
92 | }
93 | }
94 | }
95 |
96 | for (cipher in filteredCiphers) {
97 | item {
98 | CipherCard(
99 | cipher = cipher,
100 | showCipherActions = false,
101 | onClick = {
102 | navController.navigate(
103 | CipherView(
104 | cipher.id.toString()
105 | )
106 | )
107 | },
108 | onEdit = {},
109 | onDelete = {}
110 | )
111 | }
112 | }
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangePassword.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.settings.account
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.rememberCoroutineScope
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.platform.LocalContext
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.text.input.KeyboardType
14 | import androidx.compose.ui.unit.dp
15 | import androidx.hilt.navigation.compose.hiltViewModel
16 | import androidx.navigation.NavController
17 | import dev.medzik.android.compose.rememberMutable
18 | import dev.medzik.android.compose.ui.LoadingButton
19 | import dev.medzik.android.utils.showToast
20 | import dev.medzik.librepass.android.R
21 | import dev.medzik.librepass.android.common.LibrePassViewModel
22 | import dev.medzik.librepass.android.common.haveNetworkConnection
23 | import dev.medzik.librepass.android.ui.components.TextInputField
24 | import dev.medzik.librepass.android.utils.showErrorToast
25 | import dev.medzik.librepass.client.Server
26 | import dev.medzik.librepass.client.api.UserClient
27 | import kotlinx.coroutines.Dispatchers
28 | import kotlinx.coroutines.launch
29 | import kotlinx.serialization.Serializable
30 |
31 | @Serializable
32 | object SettingsAccountChangePassword
33 |
34 | @Composable
35 | fun SettingsAccountChangePasswordScreen(
36 | navController: NavController,
37 | viewModel: LibrePassViewModel = hiltViewModel()
38 | ) {
39 | val context = LocalContext.current
40 | val credentials = viewModel.credentialRepository.get() ?: return
41 |
42 | var oldPassword by rememberMutable("")
43 | var newPassword by rememberMutable("")
44 | var newPasswordConfirm by rememberMutable("")
45 | var newPasswordHint by rememberMutable("")
46 | var loading by rememberMutable(false)
47 |
48 | val scope = rememberCoroutineScope()
49 |
50 | val userClient = UserClient(
51 | email = credentials.email,
52 | apiKey = credentials.apiKey,
53 | apiUrl = credentials.apiUrl ?: Server.PRODUCTION
54 | )
55 |
56 | fun changePassword(oldPassword: String, newPassword: String, newPasswordHint: String) {
57 | if (!context.haveNetworkConnection()) {
58 | context.showToast(R.string.Error_NoInternetConnection)
59 | return
60 | }
61 |
62 | loading = true
63 |
64 | scope.launch(Dispatchers.IO) {
65 | try {
66 | userClient.changePassword(oldPassword, newPassword, newPasswordHint)
67 |
68 | navigateToWelcomeAndLogout(viewModel, navController, credentials.userId)
69 | } catch (e: Exception) {
70 | e.showErrorToast(context)
71 |
72 | loading = false
73 | }
74 | }
75 | }
76 |
77 | TextInputField(
78 | label = stringResource(R.string.OldPassword),
79 | value = oldPassword,
80 | onValueChange = { oldPassword = it },
81 | hidden = true,
82 | emptySupportingText = true,
83 | keyboardType = KeyboardType.Password
84 | )
85 |
86 | TextInputField(
87 | label = stringResource(R.string.NewPassword),
88 | value = newPassword,
89 | onValueChange = { newPassword = it },
90 | hidden = true,
91 | isError = newPassword.isNotEmpty() && newPassword.length < 8,
92 | errorMessage = stringResource(R.string.Error_PasswordTooShort),
93 | keyboardType = KeyboardType.Password
94 | )
95 |
96 | TextInputField(
97 | label = stringResource(R.string.ConfirmNewPassword),
98 | value = newPasswordConfirm,
99 | onValueChange = { newPasswordConfirm = it },
100 | hidden = true,
101 | isError = newPasswordConfirm.isNotEmpty() && newPasswordConfirm != newPassword,
102 | errorMessage = stringResource(R.string.Error_PasswordsDoNotMatch),
103 | keyboardType = KeyboardType.Password
104 | )
105 |
106 | TextInputField(
107 | label = stringResource(R.string.PasswordHint),
108 | value = newPasswordHint,
109 | onValueChange = { newPasswordHint = it },
110 | emptySupportingText = true
111 | )
112 |
113 | LoadingButton(
114 | loading = loading,
115 | onClick = { changePassword(oldPassword, newPassword, newPasswordHint) },
116 | enabled = oldPassword.isNotEmpty() && newPassword.isNotEmpty() && newPasswordConfirm == newPassword,
117 | modifier = Modifier
118 | .fillMaxWidth()
119 | .padding(horizontal = 40.dp, vertical = 8.dp)
120 | ) {
121 | Text(stringResource(R.string.ChangePassword))
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.imePadding
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material3.Scaffold
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.LaunchedEffect
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.unit.dp
17 | import androidx.hilt.navigation.compose.hiltViewModel
18 | import androidx.lifecycle.Lifecycle
19 | import androidx.lifecycle.compose.LocalLifecycleOwner
20 | import androidx.lifecycle.compose.currentStateAsState
21 | import androidx.navigation.NavController
22 | import androidx.navigation.compose.NavHost
23 | import androidx.navigation.compose.composable
24 | import androidx.navigation.compose.rememberNavController
25 | import dev.medzik.android.compose.icons.TopAppBarBackIcon
26 | import dev.medzik.android.compose.navigation.NavigationAnimations
27 | import dev.medzik.librepass.android.MainActivity
28 | import dev.medzik.librepass.android.common.LibrePassViewModel
29 | import dev.medzik.librepass.android.ui.components.TopBar
30 | import dev.medzik.librepass.android.ui.screens.Welcome
31 | import dev.medzik.librepass.android.ui.screens.WelcomeScreen
32 | import dev.medzik.librepass.android.ui.screens.auth.Unlock
33 | import dev.medzik.librepass.android.ui.screens.auth.authNavigation
34 | import dev.medzik.librepass.android.ui.screens.settings.settingsNavigation
35 | import dev.medzik.librepass.android.ui.screens.vault.Vault
36 | import dev.medzik.librepass.android.ui.screens.vault.vaultNavigation
37 |
38 | @Composable
39 | fun LibrePassNavigation(viewModel: LibrePassViewModel = hiltViewModel()) {
40 | val context = LocalContext.current
41 | val navController = rememberNavController()
42 |
43 | // Lifecycle events handler.
44 | // This calls the `onResume` function from MainActivity when the application is resumed.
45 | // This is used to lock the vault after X minutes of application sleep in memory.
46 | val lifecycleOwner = LocalLifecycleOwner.current
47 | val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState()
48 | LaunchedEffect(lifecycleState) {
49 | when (lifecycleState) {
50 | // when the application was resumed
51 | Lifecycle.State.RESUMED -> {
52 | // calls the `onResume` function from MainActivity
53 | (context as MainActivity).onResume(navController)
54 | }
55 | // ignore any other lifecycle state
56 | else -> {}
57 | }
58 | }
59 |
60 | fun getStartRoute(): Any {
61 | // if a user is not logged in, show welcome screen
62 | viewModel.credentialRepository.get() ?: return Welcome
63 |
64 | // if user secrets are not set, show unlock screen
65 | if (viewModel.vault.aesKey.isEmpty())
66 | return Unlock
67 |
68 | // else where the user secrets are set, show vault screen
69 | return Vault
70 | }
71 |
72 | NavHost(
73 | navController,
74 | startDestination = remember { getStartRoute() },
75 | modifier = Modifier.imePadding(),
76 | enterTransition = {
77 | NavigationAnimations.enterTransition()
78 | },
79 | exitTransition = {
80 | NavigationAnimations.exitTransition()
81 | },
82 | popEnterTransition = {
83 | NavigationAnimations.popEnterTransition()
84 | },
85 | popExitTransition = {
86 | NavigationAnimations.popExitTransition()
87 | }
88 | ) {
89 | composable {
90 | WelcomeScreen(navController)
91 | }
92 |
93 | authNavigation(navController)
94 |
95 | vaultNavigation(navController)
96 |
97 | settingsNavigation(navController)
98 | }
99 | }
100 |
101 | @Composable
102 | fun DefaultScaffold(
103 | topBar: @Composable () -> Unit = {},
104 | floatingActionButton: @Composable () -> Unit = {},
105 | horizontalPadding: Boolean = true,
106 | composable: @Composable () -> Unit
107 | ) {
108 | Scaffold(
109 | topBar = { topBar() },
110 | floatingActionButton = { floatingActionButton() }
111 | ) { innerPadding ->
112 | Column(
113 | modifier = Modifier
114 | .fillMaxSize()
115 | .padding(innerPadding)
116 | .padding(horizontal = if (horizontalPadding) 16.dp else 0.dp)
117 | ) {
118 | composable()
119 | }
120 | }
121 | }
122 |
123 | @Composable
124 | fun TopBarWithBack(@StringRes title: Int, navController: NavController) {
125 | TopBar(
126 | title = stringResource(title),
127 | navigationIcon = { TopAppBarBackIcon(navController) }
128 | )
129 | }
130 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/components/TextInputField.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.components
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.text.KeyboardOptions
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.Visibility
8 | import androidx.compose.material.icons.filled.VisibilityOff
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.IconButton
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.OutlinedTextField
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.text.input.KeyboardType
19 | import androidx.compose.ui.text.input.PasswordVisualTransformation
20 | import androidx.compose.ui.text.input.VisualTransformation
21 |
22 | @Composable
23 | fun TextInputField(
24 | label: String,
25 | hidden: Boolean = false,
26 | value: String?,
27 | onValueChange: (String) -> Unit,
28 | emptySupportingText: Boolean = false,
29 | isError: Boolean = false,
30 | errorMessage: String? = null,
31 | keyboardType: KeyboardType = KeyboardType.Text
32 | ) {
33 | val hiddenState = remember { mutableStateOf(hidden) }
34 |
35 | var supportingText: @Composable (() -> Unit)? = null
36 |
37 | if (errorMessage != null) {
38 | supportingText = if (isError) {
39 | {
40 | Text(
41 | text = errorMessage,
42 | color = MaterialTheme.colorScheme.error
43 | )
44 | }
45 | } else {
46 | { Text(text = "") }
47 | }
48 | }
49 |
50 | if (emptySupportingText) {
51 | supportingText = { Text(text = "") }
52 | }
53 |
54 | OutlinedTextField(
55 | value = value ?: "",
56 | onValueChange = onValueChange,
57 | label = { Text(label) },
58 | maxLines = 1,
59 | singleLine = true,
60 | visualTransformation = (
61 | if (hidden && hiddenState.value) {
62 | PasswordVisualTransformation()
63 | } else {
64 | VisualTransformation.None
65 | }
66 | ),
67 | trailingIcon = {
68 | if (hidden) {
69 | IconButton(onClick = { hiddenState.value = !hiddenState.value }) {
70 | Icon(
71 | imageVector = (
72 | if (hiddenState.value) {
73 | Icons.Filled.Visibility
74 | } else {
75 | Icons.Filled.VisibilityOff
76 | }
77 | ),
78 | contentDescription = null
79 | )
80 | }
81 | }
82 | },
83 | supportingText = supportingText,
84 | isError = isError,
85 | keyboardOptions = KeyboardOptions(
86 | keyboardType = keyboardType
87 | ),
88 | modifier = Modifier.fillMaxWidth()
89 | )
90 | }
91 |
92 | @Composable
93 | fun TextInputFieldBase(
94 | label: String,
95 | modifier: Modifier = Modifier,
96 | hidden: Boolean = false,
97 | value: String?,
98 | isError: Boolean = false,
99 | onValueChange: (String) -> Unit,
100 | keyboardType: KeyboardType = KeyboardType.Text,
101 | singleLine: Boolean = true,
102 | trailingIcon: @Composable () -> Unit = {}
103 | ) {
104 | val hiddenState = remember { mutableStateOf(hidden) }
105 |
106 | OutlinedTextField(
107 | value = value ?: "",
108 | onValueChange = onValueChange,
109 | isError = isError,
110 | label = { Text(label) },
111 | singleLine = singleLine,
112 | visualTransformation = (
113 | if (hidden && hiddenState.value) {
114 | PasswordVisualTransformation()
115 | } else {
116 | VisualTransformation.None
117 | }
118 | ),
119 | trailingIcon = {
120 | Row {
121 | if (hidden) {
122 | IconButton(onClick = { hiddenState.value = !hiddenState.value }) {
123 | Icon(
124 | imageVector = (
125 | if (hiddenState.value) {
126 | Icons.Filled.Visibility
127 | } else {
128 | Icons.Filled.VisibilityOff
129 | }
130 | ),
131 | contentDescription = null
132 | )
133 | }
134 | }
135 |
136 | trailingIcon()
137 | }
138 | },
139 | keyboardOptions = KeyboardOptions(
140 | keyboardType = keyboardType
141 | ),
142 | modifier = modifier
143 | )
144 | }
145 |
--------------------------------------------------------------------------------
/app/src/main/res/values-nb-rNO/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - %d time
5 | - %d timer
6 |
7 |
8 | - %d minutt
9 | - %d minutter
10 |
11 | Legg til
12 | Legg til felt
13 | Legg til nytt chiffer
14 | Legg til tjener
15 | Avbryt
16 | Skru på biometrisk identitetsbekreftelse
17 | Oppsett
18 | Bruk passord
19 | Vennligst autentiser deg selv
20 | Lås opp
21 | Kortnummer
22 | Kortinnehavers navn
23 | Endre passord
24 | Kort
25 | Innloggingsdata
26 | Sikkert notat
27 | Bekreft nytt passord
28 | Bekreft passord
29 | Slett
30 | Slett konto
31 | Rediger
32 | E-post
33 | Kryptering/dekrypteringsfeil
34 | Ugyldige identitetsdetaljer
35 | Ugyldig e-postadresse
36 | Passordet stemmer ikke
37 | Passordet er for kort
38 | Passordfeltene samsvarer ikke
39 | Ukjent feil
40 | Utløpsmåned
41 | Utløpsår
42 | Vis passordhint
43 | Lås hvelv
44 | Logg inn
45 | Innloggingsdetaljer
46 | Logg ut
47 | Navn
48 | Nytt passord
49 | Notater
50 | Gammelt passord
51 | Annet
52 | Passord
53 | Passord generator
54 | Store bokstaver
55 | Lengde
56 | Tall
57 | Symboler
58 | Passordhint
59 | Registrering
60 | Lagre
61 | Sikkerhetskode
62 | Velg chiffertype
63 | Tjeneradresse
64 | Velg en tjener
65 | Legg til selvtjent
66 | Offisiell
67 | Innstillinger
68 | Konto
69 | Utseende
70 | Sikkerhet
71 | Send inn
72 | Drakt
73 | Nattsvart
74 | Mørk
75 | Lys
76 | System
77 | Umiddelbart
78 | Aldri
79 | Navn
80 | Skriv inn din e-postadresse
81 | Et passordhint ble sendt til e-postadressen
82 | Bekreft din e-postadresse
83 | Lås opp
84 | Lås opp med biometri
85 | Brukernavn
86 | Hvelv
87 | Tidsavbrudd for hvelv
88 | Vis
89 | Nettadresse
90 | Nettside
91 | Velkommen til LibrePass
92 | Valgfritt
93 |
--------------------------------------------------------------------------------
/business-logic/src/main/java/dev/medzik/librepass/android/business/VaultCache.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.business
2 |
3 | import android.content.Context
4 | import dev.medzik.android.utils.runOnIOThread
5 | import dev.medzik.libcrypto.Hex
6 | import dev.medzik.librepass.android.database.LocalCipher
7 | import dev.medzik.librepass.android.database.LocalCipherDao
8 | import dev.medzik.librepass.android.database.datastore.SecretsStore
9 | import dev.medzik.librepass.android.database.datastore.VaultTimeoutValue
10 | import dev.medzik.librepass.android.database.datastore.deleteSecretsStore
11 | import dev.medzik.librepass.android.database.datastore.readSecretsStore
12 | import dev.medzik.librepass.android.database.datastore.readVaultTimeout
13 | import dev.medzik.librepass.android.database.datastore.writeSecretsStore
14 | import dev.medzik.librepass.android.database.datastore.writeVaultTimeout
15 | import dev.medzik.librepass.types.api.SyncResponse
16 | import dev.medzik.librepass.types.cipher.Cipher
17 | import dev.medzik.librepass.types.cipher.CipherType
18 | import dev.medzik.librepass.types.cipher.EncryptedCipher
19 | import kotlinx.coroutines.runBlocking
20 | import java.util.UUID
21 |
22 | class VaultCache(private val cipherRepository: LocalCipherDao) {
23 | var aesKey: ByteArray = byteArrayOf()
24 | val ciphers = mutableListOf()
25 |
26 | fun decrypt(ciphers: List) {
27 | ciphers.forEach {
28 | val cipher = Cipher(it.encryptedCipher, aesKey)
29 | this.ciphers.add(cipher)
30 | }
31 | }
32 |
33 | fun getSecretsIfNotExpired(context: Context) {
34 | val expired = handleExpiration(context)
35 | if (!expired) {
36 | runOnIOThread {
37 | aesKey = Hex.decode(readSecretsStore(context).aesKey)
38 | }
39 | }
40 | }
41 |
42 | fun sync(response: SyncResponse) {
43 | val cacheCipherIDs: MutableList = mutableListOf()
44 | ciphers.forEach { cacheCipherIDs.add(it.id) }
45 |
46 | // delete ciphers from the local database that are not in API response
47 | for (cipherId in cacheCipherIDs) {
48 | if (cipherId !in response.ids) {
49 | delete(cipherId)
50 | }
51 | }
52 |
53 | // update ciphers
54 | for (cipher in response.ciphers) {
55 | save(cipher, needUpload = false)
56 | }
57 | }
58 |
59 | fun find(id: UUID): Cipher? = ciphers.find { it.id == id }
60 |
61 | fun save(
62 | encryptedCipher: EncryptedCipher,
63 | needUpload: Boolean = true
64 | ) = save(
65 | cipher = Cipher(encryptedCipher, aesKey),
66 | encryptedCipher = encryptedCipher,
67 | needUpload = needUpload
68 | )
69 |
70 | fun save(
71 | cipher: Cipher,
72 | encryptedCipher: EncryptedCipher? = null,
73 | needUpload: Boolean = true
74 | ) {
75 | ciphers.removeIf { it.id == cipher.id }
76 | ciphers.add(cipher)
77 |
78 | cipherRepository.insert(
79 | LocalCipher(
80 | encryptedCipher = encryptedCipher ?: EncryptedCipher(cipher, aesKey),
81 | needUpload = needUpload
82 | )
83 | )
84 | }
85 |
86 | fun delete(id: UUID) {
87 | ciphers.removeIf { it.id == id }
88 | cipherRepository.delete(id)
89 | }
90 |
91 | fun getSortedCiphers(): List {
92 | return ciphers.sortedBy {
93 | when (it.type) {
94 | CipherType.Login -> {
95 | it.loginData!!.name
96 | }
97 | CipherType.SecureNote -> {
98 | it.secureNoteData!!.title
99 | }
100 | CipherType.Card -> {
101 | it.cardData!!.name
102 | }
103 | }
104 | }
105 | }
106 |
107 | fun handleExpiration(context: Context): Boolean {
108 | val vaultTimeout = runBlocking { readVaultTimeout(context) }
109 | val currentTime = System.currentTimeMillis()
110 |
111 | if (vaultTimeout.timeout == VaultTimeoutValue.NEVER)
112 | return false
113 |
114 | if (vaultTimeout.timeout == VaultTimeoutValue.INSTANT || currentTime > vaultTimeout.expires) {
115 | deleteSecrets(context)
116 |
117 | return true
118 | }
119 |
120 | return false
121 | }
122 |
123 | fun saveVaultExpiration(context: Context) {
124 | val vaultTimeout = runBlocking { readVaultTimeout(context) }
125 |
126 | if (vaultTimeout.timeout == VaultTimeoutValue.INSTANT) {
127 | deleteSecrets(context)
128 | } else {
129 | runOnIOThread {
130 | writeSecretsStore(context, SecretsStore(Hex.encode(aesKey)))
131 | }
132 |
133 | if (vaultTimeout.timeout != VaultTimeoutValue.NEVER) {
134 | val currentTime = System.currentTimeMillis()
135 | val newExpiresTime = currentTime + (vaultTimeout.timeout.minutes * 60 * 1000)
136 | runOnIOThread {
137 | writeVaultTimeout(context, vaultTimeout.copy(expires = newExpiresTime))
138 | }
139 | }
140 | }
141 | }
142 |
143 | fun deleteSecrets(context: Context) {
144 | aesKey = byteArrayOf()
145 |
146 | runOnIOThread {
147 | deleteSecretsStore(context)
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ar/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | - %d ساعة
6 |
7 |
8 |
9 | - %d ساعة
10 |
11 |
12 |
13 | - %d دقيقة
14 |
15 |
16 |
17 | - %d دقيقة
18 |
19 | قم بتشغيل المصادقة البيومترية
20 | يلغي
21 | إضافة حقل
22 | استخدم كلمة المرور
23 | أضف الخادم
24 | يثبت
25 | إضافة شفرة جديدة
26 | إضافة
27 | حذف الحساب
28 | خطأ في التشفير/فك التشفير
29 | رقم البطاقة
30 | بيانات تسجيل الدخول
31 | تأكيد كلمة المرور
32 | تغيير كلمة المرور
33 | يحرر
34 | إسم صاحب البطاقة
35 | ملاحظة آمنة
36 | بيانات البطاقة
37 | الغاء القفل
38 | يرجى المصادقة على نفسك
39 | بريد إلكتروني
40 | تأكيد كلمة المرور الجديدة
41 | تحذف
42 | ‐بيانات الاعتماد غير صالحة
43 | عنوان بريد إلكتروني غير رسمي
44 | سنة انتهاء الخدمة
45 | فاتح
46 | كلمة المرور القديمة
47 | مولدات كلمة المرور
48 | شهر انتهاء الخدمة
49 | إعدادات
50 | الحساب
51 | عنوان الخادم
52 | خزنة القفل
53 | كلمة السر قصيرة جداً
54 | مرحبًا بك في LibrePass
55 | السجل
56 | أدخل عنوان بريدك الالكتروني
57 | يُقدِّم
58 | أبداً
59 | أسود
60 | اسم المستخدم
61 | خطأ غير معروف
62 | تفاصيل تسجيل الدخول
63 | الغاء القفل
64 | الأمن
65 | النظام الافتراضي
66 | الرسمي
67 | زمن توقف خزنة
68 | نوع الشفرة المختارة
69 | فوري
70 | يرجى التحقق من عنوان البريد الإلكتروني الخاص بك
71 | ’تسجيل الدخول
72 | الأرقام
73 | عنوان
74 | داكن
75 | كلمات المرور غير متطابقة
76 | تلميحات كلمة المرور
77 | الحروف الرأسمالية
78 | اختر الخادم
79 | عنوان موقع ويب
80 | مظهر
81 | الغاء القفل مع المقاييس الحيوية
82 | اسم
83 | سمة
84 | رمز الأمن
85 | إضافة استضافة ذاتية
86 | كلمة المرور
87 | خزنة
88 | منظر
89 | تفاصيل أخرى
90 | اختياري
91 | موقع إلكتروني
92 | ملحوظات
93 | طول
94 | حفظ
95 | تسجيل خروج
96 | كلمة المرور الجديدة
97 | تم إرسال تلميح كلمة المرور إلى عنوان البريد الإلكتروني
98 | الحصول على تلميحات كلمة المرور
99 | كلمة المرور غير صحيحة
100 | الرمز
101 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsSecurity.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.settings
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.Fingerprint
7 | import androidx.compose.material.icons.filled.Timer
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.rememberCoroutineScope
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.res.pluralStringResource
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.unit.dp
20 | import androidx.hilt.navigation.compose.hiltViewModel
21 | import dev.medzik.android.compose.rememberMutable
22 | import dev.medzik.android.compose.ui.IconBox
23 | import dev.medzik.android.compose.ui.dialog.PickerDialog
24 | import dev.medzik.android.compose.ui.dialog.rememberDialogState
25 | import dev.medzik.android.compose.ui.preference.PropertyPreference
26 | import dev.medzik.android.compose.ui.preference.SwitcherPreference
27 | import dev.medzik.android.crypto.KeyStore
28 | import dev.medzik.android.utils.runOnIOThread
29 | import dev.medzik.librepass.android.MainActivity
30 | import dev.medzik.librepass.android.R
31 | import dev.medzik.librepass.android.common.LibrePassViewModel
32 | import dev.medzik.librepass.android.database.datastore.VaultTimeoutValue
33 | import dev.medzik.librepass.android.database.datastore.readVaultTimeout
34 | import dev.medzik.librepass.android.database.datastore.writeVaultTimeout
35 | import dev.medzik.librepass.android.utils.KeyAlias
36 | import dev.medzik.librepass.android.utils.checkIfBiometricAvailable
37 | import dev.medzik.librepass.android.utils.showBiometricPromptForSetup
38 | import kotlinx.coroutines.Dispatchers
39 | import kotlinx.coroutines.launch
40 | import kotlinx.serialization.Serializable
41 |
42 | @Serializable
43 | object SettingsSecurity
44 |
45 | @Composable
46 | fun SettingsSecurityScreen(viewModel: LibrePassViewModel = hiltViewModel()) {
47 | val context = LocalContext.current
48 |
49 | val credentials = viewModel.credentialRepository.get() ?: return
50 |
51 | val scope = rememberCoroutineScope()
52 | var biometricEnabled by remember { mutableStateOf(credentials.biometricAesKey != null) }
53 | val timerDialogState = rememberDialogState()
54 | var vaultTimeout by rememberMutable(readVaultTimeout(context))
55 |
56 | // Biometric checked event handler (enable/disable biometric authentication)
57 | fun biometricHandler() {
58 | if (biometricEnabled) {
59 | biometricEnabled = false
60 |
61 | scope.launch(Dispatchers.IO) {
62 | viewModel.credentialRepository.update(
63 | credentials.copy(
64 | biometricReSetup = false,
65 | biometricAesKey = null,
66 | biometricAesKeyIV = null
67 | )
68 | )
69 | }
70 |
71 | return
72 | }
73 |
74 | showBiometricPromptForSetup(
75 | context as MainActivity,
76 | cipher = KeyStore.initForEncryption(
77 | KeyAlias.BiometricAesKey,
78 | deviceAuthentication = false
79 | ),
80 | onAuthenticationSucceeded = { cipher ->
81 | val encryptedData = KeyStore.encrypt(
82 | cipher = cipher,
83 | clearBytes = viewModel.vault.aesKey
84 | )
85 |
86 | biometricEnabled = true
87 |
88 | scope.launch {
89 | viewModel.credentialRepository.update(
90 | credentials.copy(
91 | biometricAesKey = encryptedData.cipherText,
92 | biometricAesKeyIV = encryptedData.initializationVector
93 | )
94 | )
95 | }
96 | },
97 | onAuthenticationFailed = {}
98 | )
99 | }
100 |
101 | @Composable
102 | fun getVaultTimeoutTranslation(value: VaultTimeoutValue): String {
103 | return when (value) {
104 | VaultTimeoutValue.INSTANT -> stringResource(R.string.Timeout_Instant)
105 |
106 | VaultTimeoutValue.ONE_MINUTE -> pluralStringResource(R.plurals.minutes, 1, 1)
107 |
108 | VaultTimeoutValue.FIVE_MINUTES -> pluralStringResource(R.plurals.minutes, 5, 5)
109 |
110 | VaultTimeoutValue.FIFTEEN_MINUTES -> pluralStringResource(R.plurals.minutes, 15, 15)
111 |
112 | VaultTimeoutValue.THIRTY_MINUTES -> pluralStringResource(R.plurals.minutes, 30, 30)
113 |
114 | VaultTimeoutValue.ONE_HOUR -> pluralStringResource(R.plurals.hours, 1, 1)
115 |
116 | VaultTimeoutValue.NEVER -> stringResource(R.string.Timeout_Never)
117 | }
118 | }
119 |
120 | if (checkIfBiometricAvailable(context)) {
121 | SwitcherPreference(
122 | title = stringResource(R.string.UnlockWithBiometrics),
123 | leading = { IconBox(Icons.Default.Fingerprint) },
124 | checked = biometricEnabled,
125 | onCheckedChange = { biometricHandler() }
126 | )
127 | }
128 |
129 | PropertyPreference(
130 | title = stringResource(R.string.VaultTimeout),
131 | leading = { IconBox(Icons.Default.Timer) },
132 | currentValue = getVaultTimeoutTranslation(vaultTimeout.timeout),
133 | onClick = { timerDialogState.show() },
134 | )
135 |
136 | PickerDialog(
137 | state = timerDialogState,
138 | title = stringResource(R.string.VaultTimeout),
139 | items = VaultTimeoutValue.entries,
140 | onSelected = {
141 | vaultTimeout = vaultTimeout.copy(timeout = it)
142 | runOnIOThread { writeVaultTimeout(context, vaultTimeout) }
143 | }
144 | ) {
145 | Text(
146 | text = getVaultTimeoutTranslation(it),
147 | modifier = Modifier
148 | .padding(vertical = 12.dp)
149 | .fillMaxWidth()
150 | )
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/auth/Register.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.auth
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.text.KeyboardOptions
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Email
10 | import androidx.compose.material.icons.filled.QuestionMark
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.rememberCoroutineScope
16 | import androidx.compose.runtime.setValue
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.platform.LocalContext
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.text.input.KeyboardType
21 | import androidx.compose.ui.unit.dp
22 | import androidx.navigation.NavController
23 | import dev.medzik.android.compose.rememberMutable
24 | import dev.medzik.android.compose.ui.LoadingButton
25 | import dev.medzik.android.compose.ui.textfield.AnimatedTextField
26 | import dev.medzik.android.compose.ui.textfield.PasswordAnimatedTextField
27 | import dev.medzik.android.compose.ui.textfield.TextFieldValue
28 | import dev.medzik.android.utils.runOnUiThread
29 | import dev.medzik.android.utils.showToast
30 | import dev.medzik.librepass.android.R
31 | import dev.medzik.librepass.android.common.haveNetworkConnection
32 | import dev.medzik.librepass.android.common.popUpToStartDestination
33 | import dev.medzik.librepass.android.ui.components.auth.ChoiceServer
34 | import dev.medzik.librepass.android.utils.showErrorToast
35 | import dev.medzik.librepass.client.Server
36 | import dev.medzik.librepass.client.api.AuthClient
37 | import kotlinx.coroutines.Dispatchers
38 | import kotlinx.coroutines.launch
39 | import kotlinx.serialization.Serializable
40 |
41 | @Serializable
42 | object Register
43 |
44 | @Composable
45 | fun RegisterScreen(navController: NavController) {
46 | val context = LocalContext.current
47 |
48 | val scope = rememberCoroutineScope()
49 |
50 | var loading by rememberMutable(false)
51 | val email = rememberMutable("")
52 | val password = rememberMutable("")
53 | val confirmPassword = rememberMutable("")
54 | val passwordHint = rememberMutable("")
55 | val server = rememberMutable(Server.PRODUCTION)
56 |
57 | // Register user with given credentials and navigate to log in screen.
58 | fun submit(email: String, password: String, passwordHint: String?) {
59 | if (!context.haveNetworkConnection()) {
60 | context.showToast(R.string.Error_NoInternetConnection)
61 | return
62 | }
63 |
64 | val authClient = AuthClient(apiUrl = server.value)
65 |
66 | // disable button
67 | loading = true
68 |
69 | scope.launch(Dispatchers.IO) {
70 | try {
71 | authClient.register(email, password, passwordHint)
72 |
73 | // navigate to login
74 | runOnUiThread {
75 | context.showToast(R.string.Toast_PleaseVerifyYourEmail)
76 |
77 | navController.navigate(Login) {
78 | popUpToStartDestination(navController)
79 | }
80 | }
81 | } catch (e: Exception) {
82 | loading = false
83 | e.showErrorToast(context)
84 | }
85 | }
86 | }
87 |
88 | Column(
89 | verticalArrangement = Arrangement.spacedBy(15.dp)
90 | ) {
91 | AnimatedTextField(
92 | label = stringResource(R.string.Email),
93 | value = TextFieldValue.fromMutableState(email),
94 | keyboardOptions = KeyboardOptions(
95 | keyboardType = KeyboardType.Email
96 | ),
97 | leading = {
98 | Icon(
99 | Icons.Default.Email,
100 | contentDescription = null
101 | )
102 | }
103 | )
104 |
105 | PasswordAnimatedTextField(
106 | label = stringResource(R.string.Password),
107 | value = TextFieldValue.fromMutableState(
108 | state = password,
109 | error = if (password.value.isNotEmpty() && password.value.length < 8) {
110 | stringResource(R.string.Error_PasswordTooShort)
111 | } else null
112 | ),
113 | keyboardOptions = KeyboardOptions(
114 | keyboardType = KeyboardType.Email
115 | )
116 | )
117 |
118 | PasswordAnimatedTextField(
119 | label = stringResource(R.string.ConfirmPassword),
120 | value = TextFieldValue.fromMutableState(
121 | state = confirmPassword,
122 | error = if (confirmPassword.value.isNotEmpty() && password.value != confirmPassword.value) {
123 | stringResource(R.string.Error_PasswordsDoNotMatch)
124 | } else null
125 | ),
126 | keyboardOptions = KeyboardOptions(
127 | keyboardType = KeyboardType.Email
128 | )
129 | )
130 |
131 | AnimatedTextField(
132 | label = stringResource(R.string.PasswordHint),
133 | value = TextFieldValue.fromMutableState(
134 | passwordHint,
135 | valueLabel = TextFieldValue.ValueLabel(
136 | type = TextFieldValue.ValueLabel.Type.INFO,
137 | text = stringResource(R.string.Optional)
138 | )
139 | ),
140 | leading = {
141 | Icon(
142 | Icons.Default.QuestionMark,
143 | contentDescription = null
144 | )
145 | }
146 | )
147 | }
148 |
149 | ChoiceServer(navController, server)
150 |
151 | val isError = !email.value.contains("@") ||
152 | password.value.length < 8 ||
153 | confirmPassword.value != password.value
154 |
155 | LoadingButton(
156 | loading = loading,
157 | onClick = { submit(email.value, password.value, passwordHint.value) },
158 | enabled = !isError,
159 | modifier = Modifier
160 | .fillMaxWidth()
161 | .padding(horizontal = 40.dp)
162 | ) {
163 | Text(stringResource(R.string.Register))
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherAdd.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.vault
2 |
3 | import android.os.Parcelable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.rememberScrollState
8 | import androidx.compose.foundation.verticalScroll
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.rememberCoroutineScope
15 | import androidx.compose.runtime.setValue
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.unit.dp
20 | import androidx.hilt.navigation.compose.hiltViewModel
21 | import androidx.navigation.NavController
22 | import dev.medzik.android.compose.icons.TopAppBarBackIcon
23 | import dev.medzik.android.compose.rememberMutable
24 | import dev.medzik.android.compose.ui.LoadingButton
25 | import dev.medzik.android.utils.runOnUiThread
26 | import dev.medzik.librepass.android.R
27 | import dev.medzik.librepass.android.common.LibrePassViewModel
28 | import dev.medzik.librepass.android.common.parceler.CipherTypeParceler
29 | import dev.medzik.librepass.android.ui.components.CipherEditFieldsCard
30 | import dev.medzik.librepass.android.ui.components.CipherEditFieldsLogin
31 | import dev.medzik.librepass.android.ui.components.CipherEditFieldsSecureNote
32 | import dev.medzik.librepass.android.ui.components.TopBar
33 | import dev.medzik.librepass.android.utils.showErrorToast
34 | import dev.medzik.librepass.types.cipher.Cipher
35 | import dev.medzik.librepass.types.cipher.CipherType
36 | import dev.medzik.librepass.types.cipher.data.CipherCardData
37 | import dev.medzik.librepass.types.cipher.data.CipherLoginData
38 | import dev.medzik.librepass.types.cipher.data.CipherSecureNoteData
39 | import kotlinx.coroutines.Dispatchers
40 | import kotlinx.coroutines.launch
41 | import kotlinx.parcelize.Parcelize
42 | import kotlinx.parcelize.TypeParceler
43 | import kotlinx.serialization.Serializable
44 | import java.util.UUID
45 |
46 | @Parcelize
47 | @TypeParceler()
48 | @Serializable
49 | data class CipherAdd(val cipherType: CipherType): Parcelable
50 |
51 | @Composable
52 | fun CipherAddScreen(
53 | navController: NavController,
54 | args: CipherAdd,
55 | viewModel: LibrePassViewModel = hiltViewModel()
56 | ) {
57 | val context = LocalContext.current
58 |
59 | val scope = rememberCoroutineScope()
60 | val credentials = remember { viewModel.credentialRepository.get() } ?: return
61 |
62 | var cipher by rememberMutable(
63 | Cipher(
64 | id = UUID.randomUUID(),
65 | owner = credentials.userId,
66 | type = args.cipherType,
67 | loginData = if (args.cipherType == CipherType.Login) {
68 | CipherLoginData(name = "")
69 | } else null,
70 | cardData = if (args.cipherType == CipherType.Card) {
71 | CipherCardData(name = "", cardholderName = "", number = "")
72 | } else null,
73 | secureNoteData = if (args.cipherType == CipherType.SecureNote) {
74 | CipherSecureNoteData(title = "", note = "")
75 | } else null
76 | )
77 | )
78 |
79 | var loading by rememberMutable(false)
80 |
81 | fun submit() {
82 | loading = true
83 |
84 | scope.launch(Dispatchers.IO) {
85 | try {
86 | viewModel.vault.save(cipher)
87 |
88 | runOnUiThread { navController.popBackStack() }
89 | } catch (e: Exception) {
90 | loading = false
91 | e.showErrorToast(context)
92 | }
93 | }
94 | }
95 |
96 | @Composable
97 | fun buttonEnabled(): Boolean {
98 | return when (cipher.type) {
99 | CipherType.Login -> {
100 | cipher.loginData!!.name.isNotEmpty()
101 | }
102 |
103 | CipherType.SecureNote -> {
104 | cipher.secureNoteData!!.title.isNotEmpty() &&
105 | cipher.secureNoteData!!.note.isNotEmpty()
106 | }
107 |
108 | CipherType.Card -> {
109 | cipher.cardData!!.name.isNotEmpty() &&
110 | cipher.cardData!!.cardholderName.isNotEmpty() &&
111 | cipher.cardData!!.number.isNotEmpty()
112 | }
113 | }
114 | }
115 |
116 | Scaffold(
117 | topBar = {
118 | TopBar(
119 | title = stringResource(R.string.AddNewCipher),
120 | navigationIcon = { TopAppBarBackIcon(navController) }
121 | )
122 | }
123 | ) { innerPadding ->
124 | Column(
125 | modifier = Modifier
126 | .padding(innerPadding)
127 | .padding(horizontal = 16.dp)
128 | .verticalScroll(rememberScrollState())
129 | ) {
130 | @Composable
131 | fun button(): @Composable (cipher: Cipher) -> Unit {
132 | return {
133 | cipher = it
134 |
135 | LoadingButton(
136 | loading = loading,
137 | onClick = { submit() },
138 | enabled = buttonEnabled(),
139 | modifier = Modifier
140 | .fillMaxWidth()
141 | .padding(top = 16.dp)
142 | .padding(horizontal = 40.dp)
143 | ) {
144 | Text(stringResource(R.string.Add))
145 | }
146 | }
147 | }
148 |
149 | when (cipher.type) {
150 | CipherType.Login -> {
151 | CipherEditFieldsLogin(
152 | navController,
153 | cipher,
154 | button()
155 | )
156 | }
157 |
158 | CipherType.SecureNote -> {
159 | CipherEditFieldsSecureNote(
160 | cipher,
161 | button()
162 | )
163 | }
164 |
165 | CipherType.Card -> {
166 | CipherEditFieldsCard(
167 | cipher,
168 | button()
169 | )
170 | }
171 | }
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/vault/CipherEdit.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.vault
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material3.Scaffold
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.rememberCoroutineScope
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.compose.ui.unit.dp
19 | import androidx.hilt.navigation.compose.hiltViewModel
20 | import androidx.navigation.NavController
21 | import dev.medzik.android.compose.icons.TopAppBarBackIcon
22 | import dev.medzik.android.compose.rememberMutable
23 | import dev.medzik.android.compose.ui.LoadingButton
24 | import dev.medzik.android.utils.runOnUiThread
25 | import dev.medzik.librepass.android.R
26 | import dev.medzik.librepass.android.common.LibrePassViewModel
27 | import dev.medzik.librepass.android.ui.components.CipherEditFieldsCard
28 | import dev.medzik.librepass.android.ui.components.CipherEditFieldsLogin
29 | import dev.medzik.librepass.android.ui.components.CipherEditFieldsSecureNote
30 | import dev.medzik.librepass.android.ui.components.TopBar
31 | import dev.medzik.librepass.android.utils.showErrorToast
32 | import dev.medzik.librepass.types.cipher.Cipher
33 | import dev.medzik.librepass.types.cipher.CipherType
34 | import dev.medzik.librepass.types.cipher.data.PasswordHistory
35 | import dev.medzik.otp.OTPParameters
36 | import dev.medzik.otp.TOTPGenerator
37 | import kotlinx.coroutines.Dispatchers
38 | import kotlinx.coroutines.launch
39 | import kotlinx.serialization.Serializable
40 | import java.util.Date
41 | import java.util.UUID
42 |
43 | @Serializable
44 | data class CipherEdit(val cipherId: String)
45 |
46 | @Composable
47 | fun CipherEditScreen(
48 | navController: NavController,
49 | args: CipherEdit,
50 | viewModel: LibrePassViewModel = hiltViewModel()
51 | ) {
52 | val context = LocalContext.current
53 |
54 | val scope = rememberCoroutineScope()
55 |
56 | val oldCipher = remember { viewModel.vault.find(UUID.fromString(args.cipherId)) } ?: return
57 | var cipher by rememberMutable(oldCipher)
58 |
59 | var loading by rememberMutable(false)
60 |
61 | fun submit() {
62 | loading = true
63 |
64 | scope.launch(Dispatchers.IO) {
65 | if (cipher.type == CipherType.Login) {
66 | val basePassword = oldCipher.loginData!!.password
67 | val newPassword = cipher.loginData!!.password
68 |
69 | if (basePassword != null && basePassword != newPassword) {
70 | val newList = mutableListOf()
71 | val oldList = oldCipher.loginData!!.passwordHistory
72 | if (oldList != null) newList.addAll(oldList)
73 |
74 | newList.add(PasswordHistory(basePassword, Date()))
75 |
76 | cipher = cipher.copy(loginData = cipher.loginData!!.copy(passwordHistory = newList))
77 | }
78 | }
79 |
80 | try {
81 | viewModel.vault.save(cipher)
82 |
83 | runOnUiThread { navController.popBackStack() }
84 | } catch (e: Exception) {
85 | loading = false
86 | e.showErrorToast(context)
87 | }
88 | }
89 | }
90 |
91 | @Composable
92 | fun buttonEnabled(): Boolean {
93 | return when (cipher.type) {
94 | CipherType.Login -> {
95 | cipher.loginData!!.name.isNotEmpty() &&
96 | (
97 | cipher.loginData!!.twoFactor.isNullOrBlank() ||
98 | runCatching {
99 | val params = OTPParameters.parseUrl(cipher.loginData?.twoFactor)
100 | TOTPGenerator.now(params)
101 | }.isSuccess
102 | )
103 | }
104 |
105 | CipherType.SecureNote -> {
106 | cipher.secureNoteData!!.title.isNotEmpty() &&
107 | cipher.secureNoteData!!.note.isNotEmpty()
108 | }
109 |
110 | CipherType.Card -> {
111 | cipher.cardData!!.name.isNotEmpty() &&
112 | cipher.cardData!!.cardholderName.isNotEmpty() &&
113 | cipher.cardData!!.number.isNotEmpty()
114 | }
115 | }
116 | }
117 |
118 | Scaffold(
119 | topBar = {
120 | TopBar(
121 | title = stringResource(R.string.AddNewCipher),
122 | navigationIcon = { TopAppBarBackIcon(navController) }
123 | )
124 | }
125 | ) { innerPadding ->
126 | Column(
127 | modifier = Modifier
128 | .padding(innerPadding)
129 | .padding(horizontal = 16.dp)
130 | .verticalScroll(rememberScrollState())
131 | ) {
132 | @Composable
133 | fun button(): @Composable (cipher: Cipher) -> Unit {
134 | return {
135 | cipher = it
136 |
137 | LoadingButton(
138 | loading = loading,
139 | onClick = { submit() },
140 | enabled = buttonEnabled(),
141 | modifier =
142 | Modifier
143 | .fillMaxWidth()
144 | .padding(top = 16.dp)
145 | .padding(horizontal = 40.dp)
146 | ) {
147 | Text(stringResource(R.string.Save))
148 | }
149 | }
150 | }
151 |
152 | when (cipher.type) {
153 | CipherType.Login -> {
154 | CipherEditFieldsLogin(
155 | navController,
156 | cipher,
157 | button()
158 | )
159 | }
160 |
161 | CipherType.SecureNote -> {
162 | CipherEditFieldsSecureNote(
163 | cipher,
164 | button()
165 | )
166 | }
167 |
168 | CipherType.Card -> {
169 | CipherEditFieldsCard(
170 | cipher,
171 | button()
172 | )
173 | }
174 | }
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/medzik/librepass/android/ui/screens/auth/Login.kt:
--------------------------------------------------------------------------------
1 | package dev.medzik.librepass.android.ui.screens.auth
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.text.KeyboardOptions
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.filled.Email
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.runtime.rememberCoroutineScope
15 | import androidx.compose.runtime.setValue
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.text.input.KeyboardType
20 | import androidx.compose.ui.unit.dp
21 | import androidx.hilt.navigation.compose.hiltViewModel
22 | import androidx.navigation.NavController
23 | import dev.medzik.android.compose.rememberMutable
24 | import dev.medzik.android.compose.ui.LoadingButton
25 | import dev.medzik.android.compose.ui.textfield.AnimatedTextField
26 | import dev.medzik.android.compose.ui.textfield.PasswordAnimatedTextField
27 | import dev.medzik.android.compose.ui.textfield.TextFieldValue
28 | import dev.medzik.android.utils.runOnUiThread
29 | import dev.medzik.android.utils.showToast
30 | import dev.medzik.librepass.android.R
31 | import dev.medzik.librepass.android.common.LibrePassViewModel
32 | import dev.medzik.librepass.android.common.haveNetworkConnection
33 | import dev.medzik.librepass.android.common.popUpToStartDestination
34 | import dev.medzik.librepass.android.database.Credentials
35 | import dev.medzik.librepass.android.ui.components.auth.ChoiceServer
36 | import dev.medzik.librepass.android.ui.screens.vault.Vault
37 | import dev.medzik.librepass.android.utils.showErrorToast
38 | import dev.medzik.librepass.client.Server
39 | import dev.medzik.librepass.client.api.AuthClient
40 | import dev.medzik.librepass.utils.fromHex
41 | import kotlinx.coroutines.Dispatchers
42 | import kotlinx.coroutines.launch
43 | import kotlinx.serialization.Serializable
44 |
45 | @Serializable
46 | object Login
47 |
48 | @Composable
49 | fun LoginScreen(
50 | navController: NavController,
51 | viewModel: LibrePassViewModel = hiltViewModel()
52 | ) {
53 | val context = LocalContext.current
54 |
55 | val scope = rememberCoroutineScope()
56 |
57 | var loading by rememberMutable(false)
58 | val email = rememberMutable("")
59 | val password = rememberMutable("")
60 | val server = rememberMutable(Server.PRODUCTION)
61 |
62 | fun submit(email: String, password: String) {
63 | if (!context.haveNetworkConnection()) {
64 | context.showToast(R.string.Error_NoInternetConnection)
65 | return
66 | }
67 |
68 | val authClient = AuthClient(apiUrl = server.value)
69 |
70 | if (email.isEmpty() || password.isEmpty())
71 | return
72 |
73 | loading = true
74 |
75 | scope.launch(Dispatchers.IO) {
76 | try {
77 | val preLogin = authClient.preLogin(email)
78 |
79 | val credentials = authClient.login(
80 | email = email,
81 | password = password
82 | )
83 |
84 | // save credentials
85 | val credentialsDb = Credentials(
86 | userId = credentials.userId,
87 | email = email,
88 | apiUrl = if (server.value == Server.PRODUCTION) null else server.value,
89 | apiKey = credentials.apiKey,
90 | publicKey = credentials.publicKey,
91 | // Argon2id parameters
92 | memory = preLogin.memory,
93 | iterations = preLogin.iterations,
94 | parallelism = preLogin.parallelism
95 | )
96 | viewModel.credentialRepository.insert(credentialsDb)
97 |
98 | viewModel.vault.aesKey = credentials.aesKey.fromHex()
99 |
100 | viewModel.credentialRepository.update(
101 | credentialsDb.copy(
102 | biometricReSetup = true
103 | )
104 | )
105 |
106 | runOnUiThread {
107 | navController.navigate(Vault) {
108 | popUpToStartDestination(navController)
109 | }
110 | }
111 | } catch (e: Exception) {
112 | loading = false
113 | e.showErrorToast(context)
114 | }
115 | }
116 | }
117 |
118 | AnimatedTextField(
119 | label = stringResource(R.string.Email),
120 | value = TextFieldValue.fromMutableState(email),
121 | keyboardOptions = KeyboardOptions(
122 | keyboardType = KeyboardType.Email
123 | ),
124 | leading = {
125 | Icon(
126 | Icons.Default.Email,
127 | contentDescription = null
128 | )
129 | }
130 | )
131 |
132 | fun requestPasswordHint() {
133 | val authClient = AuthClient(apiUrl = server.value)
134 |
135 | if (email.value.isEmpty()) {
136 | context.showToast(context.getString(R.string.Toast_Enter_Email))
137 | return
138 | }
139 |
140 | scope.launch(Dispatchers.IO) {
141 | try {
142 | authClient.requestPasswordHint(email.value)
143 |
144 | context.showToast(context.getString(R.string.Toast_Password_Hint_Sent))
145 | } catch (e: Exception) {
146 | e.showErrorToast(context)
147 | }
148 | }
149 | }
150 |
151 | Text(
152 | text = stringResource(R.string.GetPasswordHint),
153 | style = MaterialTheme.typography.bodyMedium,
154 | color = MaterialTheme.colorScheme.primary,
155 | modifier = Modifier
156 | .padding(vertical = 8.dp)
157 | .clickable { requestPasswordHint() }
158 | )
159 |
160 | PasswordAnimatedTextField(
161 | label = stringResource(R.string.Password),
162 | value = TextFieldValue.fromMutableState(password),
163 | keyboardOptions = KeyboardOptions(
164 | keyboardType = KeyboardType.Email
165 | )
166 | )
167 |
168 | ChoiceServer(navController, server)
169 |
170 | LoadingButton(
171 | loading = loading,
172 | onClick = { submit(email.value, password.value) },
173 | enabled = email.value.isNotEmpty() && password.value.isNotEmpty(),
174 | modifier = Modifier
175 | .fillMaxWidth()
176 | .padding(horizontal = 40.dp)
177 | ) {
178 | Text(stringResource(R.string.Login))
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/app/src/main/res/values-hi/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | जोड़ें
4 | कृपया स्वयं को प्रमाणित करें
5 | अनलॉक
6 | कार्ड नंबर
7 | कार्डधारक का नाम
8 | पासवर्ड बदलें
9 | कार्ड डेटा
10 | लॉगिन डेटा
11 | सुरक्षित नोट
12 | नए पासवर्ड की पुष्टि करें
13 | पासवर्ड पुष्टि करें
14 | मिटाएं
15 | खाता मिटाएं
16 | संपादित करें
17 | ई-मेल
18 | एन्क्रिप्शन/डिक्रिप्शन त्रुटि
19 | अमान्य क्रेडेंशियल
20 | अमान्य ई-मेल पता
21 | पासवर्ड गलत है
22 | पासवर्ड बहुत छोटा है
23 | पासवर्ड मेल नहीं खाते
24 | अज्ञात त्रुटि
25 | समाप्ति महीना
26 | समाप्ति वर्ष
27 | पासवर्ड संकेत प्राप्त करें
28 | लॉग इन
29 | लॉगइन विवरण
30 | लॉग आउट
31 | नाम
32 | नया पासवर्ड
33 | अन्य विवरण
34 | पासवर्ड जनरेटर
35 | बड़े अक्षर
36 | लंबाई
37 | संख्याएं
38 | पासवर्ड संकेत
39 | पंजीकृत करें
40 | सहेजें
41 | साइफर प्रकार चुनें
42 | सर्वर पता
43 | स्वयं-होस्टेड जोड़ें
44 | आधिकारिक
45 | सेटिंग्स
46 | दिखावट
47 | सुरक्षा
48 | जमा करें
49 | थीम
50 | काली
51 | गहरी
52 | हल्की
53 | सिस्टम डिफॉल्ट
54 | शीर्षक
55 | अपना ई-मेल पता दर्ज करें
56 | ईमेल पते पर पासवर्ड संकेत भेजा गया था
57 | खोलें
58 | बायोमेट्रिक्स से खोलें
59 | सर्वर चुनें
60 |
61 | - %d मिनट
62 | - %d मिनट
63 |
64 |
65 | - %d घंटा
66 | - %d घंटे
67 |
68 | नया cipher जोड़ें
69 | बायोमेट्रिक प्रमाणीकरण चालू करें
70 | पासवर्ड का प्रयोग करें
71 | टिप्पणियां
72 | कृपया अपना ई-मेल पता सत्यापित करें
73 | सर्वर जोड़े
74 | सेटअप
75 | पुराना पासवर्ड
76 | क्षेत्र जोड़ें
77 | रद्द करें
78 | पासवर्ड
79 | उपयोक्तानाम
80 | प्रतीक
81 | देखें
82 | सुरक्षा कोड
83 | कभी नहीं
84 | खाता
85 | वेबसाइट
86 | वेबसाइट पता
87 | तुरंत
88 | वैकल्पिक
89 | कार्ड विवरण
90 | नया ई-मेल पता
91 | ई-मेल पता बदलें
92 | बायोमेट्रिक कुंजी को एंड्रॉइड द्वारा अमान्य कर दिया गया है
93 | URI पता अमान्य है
94 | वॉल्ट बंद करें
95 | लिब्रेपास में आपका स्वागत है
96 | वॉल्ट
97 | वॉल्ट टाइमआउट
98 | द्वि-कारक प्रमाणीकरण
99 | द्वि-कारक रहस्य
100 | साइफर नहीं मिला
101 | संग्रह नहीं मिला
102 | अज्ञात सर्वर डाटाबेस त्रुटि
103 | दोहराया गया
104 | कृपया अपने ई-मेल पते की पुष्टि करें
105 | अमान्य API अनुरोध
106 | अवैध प्रत्यय पत्र
107 | अमान्य लॉगिन टोकन
108 | बहुत सारे अनुरोध
109 | आंतरिक सर्वर त्रुटि - ईमेल भेजने में विफल
110 | स्कैन QR कोड
111 | मैन्युअल रूप से कुंजी दर्ज करें
112 | द्वि-कारक हटाएं
113 | प्रकार
114 | एल्गोरिथ्म
115 | अंक
116 | अवधि
117 | काउंटर
118 | द्वि-कारक कॉन्फ़िगर करें
119 | कोई इंटरनेट कनेक्शन नहीं
120 | सर्वर से कोई कनेक्शन नहीं
121 |
--------------------------------------------------------------------------------
/app/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - %d dakika
5 | - %d dakika
6 |
7 |
8 | - %d saat
9 | - %d saat
10 |
11 | Ekle
12 | Alan ekle
13 | Yeni şifre ekle
14 | Sunucu ekle
15 | İki faktörlü kimlik doğrulama
16 | İki faktör sırrı
17 | İptal
18 | Biyometrik kimlik doğrulamayı aç
19 | Kurulum
20 | Şifre kullan
21 | Lütfen kimliğinizi doğrulayın
22 | Kilidi aç
23 | Kart numarası
24 | Kart sahibinin adı
25 | Şifreyi değiştir
26 | Kart verileri
27 | Oturum açma verileri
28 | Güvenli not
29 | Yeni şifreyi onayla
30 | Şifreyi onayla
31 | Sil
32 | Hesabı sil
33 | Düzenle
34 | E-posta
35 | Şifreleme/Şifre çözme hatası
36 | Geçersiz kimlik bilgileri
37 | Geçersiz e-posta adresi
38 | Şifre yanlış
39 | Şifre çok kısa
40 | Şifreler eşleşmiyor
41 | Bilinmeyen hata
42 | Son kullanım ayı
43 | Son kullanım yılı
44 | Şifre ipucunu al
45 | Kasayı kilitle
46 | Oturum aç
47 | Oturum açma bilgileri
48 | Çıkış yap
49 | Ad
50 | Yeni şifre
51 | Notlar
52 | Eski şifre
53 | Diğer bilgiler
54 | Şifre
55 | Şifre oluşturucu
56 | Büyük harfler
57 | Uzunluk
58 | Sayılar
59 | Semboller
60 | Parola ipucu
61 | Kaydol
62 | Kaydet
63 | Güvenlik kodu
64 | Şifre türünü seç
65 | Sunucu adresi
66 | Bir sunucu seç
67 | Kendi kendine barındırılanı ekle
68 | Resmi
69 | Ayarlar
70 | Hesap
71 | Görünüm
72 | Güvenlik
73 | Başvur
74 | Tema
75 | Siyah
76 | Koyu
77 | Açık
78 | Sistem varsayılanı
79 | Anında
80 | Asla
81 | Başlık
82 | E-posta adresinizi girin
83 | E-posta adresine bir şifre ipucu gönderildi
84 | Lütfen e-posta adresinizi doğrulayın
85 | Kilidi aç
86 | Biyometri ile kilidi aç
87 | Kullanıcı adı
88 | Kasa
89 | Kasa zaman aşımı
90 | Görüntüle
91 | Web sitesi adresi
92 | Web sitesi
93 | LibrePass\'a Hoş Geldiniz
94 | İsteğe bağlı
95 | URI adresi geçersiz
96 | Kart bilgileri
97 | Biyometrik anahtar Android tarafından geçersiz kılındı
98 | Yeni e-posta adresi
99 | E-posta adresini değiştirin
100 | Şifre bulunamadı
101 | Koleksiyon bulunamadı
102 | Bilinmeyen sunucu veritabanı hatası
103 | Çoğaltıldı
104 | Lütfen e-posta adresinizi doğrulayın
105 | Geçersiz API isteği
106 | Geçersiz kimlik bilgileri
107 | Geçersiz oturum açma anahtarı
108 | Çok fazla istek
109 | Dahili sunucu hatası - E-posta gönderilemedi
110 | İki faktörü yapılandır
111 | QR kodunu tara
112 | Tuşu manuel olarak gir
113 | İki faktörü sil
114 | Tür
115 | Algoritma
116 | Rakamlar
117 | Dönem
118 | Sayaç
119 | Sunucu ile bağlantı yok
120 | İnternet bağlantısı yok
121 |
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Ablaufjahr
4 | Konto löschen
5 | Hell
6 | Altes Passwort
7 | Ablaufmonat
8 | Einstellungen
9 | Konto
10 | Server-Adresse
11 | Tresor sperren
12 | Passwort ist zu kurz
13 | Willkommen bei LibrePass
14 | Registrieren
15 | Geben Sie Ihre E-Mail Adresse ein
16 | Senden
17 | Nie
18 | Schwarz
19 | Benutzername
20 | Unbekannter Fehler
21 | Entsperren
22 | Sicherheit
23 | Ungültige E-Mail
24 | Systemstandard
25 | Bearbeiten
26 | Offiziell
27 | Tresor-Zeitlimit
28 | Auswahl des Chiffrentyps
29 | Sofort
30 | Bitte bestätigen Sie Ihre E-Mail-Adresse
31 | Anmeldung
32 | Symbole
33 | Zahlen
34 | Titel
35 | Dunkel
36 | Passwörter stimmen nicht überein
37 | Passwort-Hinweis
38 | Großbuchstaben
39 | Server auswählen
40 | Erscheinungsbild
41 | Entsperren mit Biometrie
42 | Name
43 | Thema
44 | Sicherheitscode
45 | Selbst gehosteten Server hinzufügen
46 | Passwort
47 | Tresor
48 | Anzeigen
49 | Optional
50 | Notizen
51 | Länge
52 | Speichern
53 | Abmelden
54 | Löschen
55 | Neues Passwort
56 | Ein Passworthinweis wurde an die E-Mail-Adresse gesendet
57 | Passwort-Hinweis abrufen
58 | Ungültige Anmeldeinformationen
59 | Passwort ist inkorrekt
60 |
61 | - %d Stunde
62 | - %d Stunden
63 |
64 | Passwort-Generator
65 | Kontaktdaten
66 | Website-Adresse
67 |
68 | - %d Minute
69 | - %d Minuten
70 |
71 | E-Mail
72 | Sonstige Angaben
73 | Website
74 | Biometrische Authentifizierung aktivieren
75 | Verschlüsselungs-/Entschlüsselungsfehler
76 | Kartennummer
77 | Abbrechen
78 | Login-Daten
79 | Passwort bestätigen
80 | Passwort ändern
81 | Feld hinzufügen
82 | Karteninhabername
83 | Hinzufügen
84 | Sicherer Hinweis
85 | Passwort verwenden
86 | Kartendaten
87 | Entsperren
88 | Server hinzufügen
89 | Bitte authentifizieren Sie sich
90 | Neues Passwort bestätigen
91 | Einrichtung
92 | Neue Cipher hinzufügen
93 | URI Adresse ist ungültig
94 | Kartendetails
95 | Die biometrische Autorisierung wurde von Android ungültig gemacht
96 | Neue E-Mail-Adresse
97 | E-Mail-Adresse ändern
98 | Geheimnis manuell eingeben
99 | Algorithmus
100 | Ziffern
101 | Zeitraum
102 | Zähler
103 | QR-Code scannen
104 | Art
105 | Konfigurieren Sie die Zwei-Faktor-Funktion
106 | Zweiten Faktor löschen
107 | Zwei-Faktor-Authentifizierung
108 | Zwei-Faktor Geheimnis
109 | Zu viele Anfragen
110 | Interner Serverfehler - E-Mail kann nicht gesendet werden
111 | Dupliziert
112 | Unbekannter Server-Datenbankfehler
113 | Cipher nicht gefunden
114 | Ordner nicht gefunden
115 | Bitte verifizieren Sie Ihre E-Mail-Adresse
116 | Ungültige API-Anfrage
117 | Ungültige Zugangsdaten
118 | Ungültiges Login-Token
119 |
--------------------------------------------------------------------------------