mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 08:17:57 -04:00
Add android linting checks and integrate in build process (#846)
This commit is contained in:
54
apps/mobile-app/android/.editorconfig
Normal file
54
apps/mobile-app/android/.editorconfig
Normal file
@@ -0,0 +1,54 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
max_line_length = 160
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{kt,kts}]
|
||||
ktlint_standard_max-line-length = 160
|
||||
ktlint_standard_trailing-comma-on-call-site = true
|
||||
ktlint_standard_trailing-comma-on-declaration-site = true
|
||||
ktlint_standard_wrapping = true
|
||||
ktlint_standard_filename = true
|
||||
ktlint_standard_import-ordering = true
|
||||
ktlint_standard_annotation = true
|
||||
ktlint_standard_comment-spacing = true
|
||||
ktlint_standard_curly-spacing = true
|
||||
ktlint_standard_discouraged-comment-location = true
|
||||
ktlint_standard_empty-blocks = true
|
||||
ktlint_standard_enum-entry-name-case = true
|
||||
ktlint_standard_final-newline = true
|
||||
ktlint_standard_fun-keyword-spacing = true
|
||||
ktlint_standard_import-ordering = true
|
||||
ktlint_standard_import-spacing = true
|
||||
ktlint_standard_indent = true
|
||||
ktlint_standard_initializer-list = true
|
||||
ktlint_standard_ktlint_standard_max-line-length = true
|
||||
ktlint_standard_modifier-order = true
|
||||
ktlint_standard_no-blank-line-before-rbrace = true
|
||||
ktlint_standard_no-consecutive-blank-lines = true
|
||||
ktlint_standard_no-empty-file = true
|
||||
ktlint_standard_no-line-break-after-else = true
|
||||
ktlint_standard_no-line-break-before-assignment = true
|
||||
ktlint_standard_no-multi-spaces = true
|
||||
ktlint_standard_no-semi = true
|
||||
ktlint_standard_no-trailing-spaces = true
|
||||
ktlint_standard_no-unit-return = true
|
||||
ktlint_standard_no-unused-imports = true
|
||||
ktlint_standard_no-wildcard-imports = true
|
||||
ktlint_standard_parameter-list-wrapping = true
|
||||
ktlint_standard_spacing-around-colon = true
|
||||
ktlint_standard_spacing-around-comma = true
|
||||
ktlint_standard_spacing-around-dot = true
|
||||
ktlint_standard_spacing-around-keyword = true
|
||||
ktlint_standard_spacing-around-operators = true
|
||||
ktlint_standard_spacing-around-parens = true
|
||||
ktlint_standard_spacing-around-range = true
|
||||
ktlint_standard_string-formatting = true
|
||||
ktlint_standard_trailing-comma-on-call-site = true
|
||||
ktlint_standard_trailing-comma-on-declaration-site = true
|
||||
ktlint_standard_trailing-spaces = true
|
||||
ktlint_standard_wrapping = true
|
||||
@@ -1,6 +1,7 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: "org.jlleitschuh.gradle.ktlint"
|
||||
|
||||
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||
|
||||
@@ -124,6 +125,13 @@ android {
|
||||
androidResources {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
lintOptions {
|
||||
checkReleaseBuilds true
|
||||
abortOnError true
|
||||
xmlReport true
|
||||
htmlReport true
|
||||
lintConfig file("lint.xml")
|
||||
}
|
||||
}
|
||||
|
||||
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||
@@ -191,3 +199,45 @@ dependencies {
|
||||
}
|
||||
implementation "org.jetbrains.kotlin:kotlin-test:1.9.25"
|
||||
}
|
||||
|
||||
ktlint {
|
||||
android = true
|
||||
verbose = true
|
||||
outputToConsole = true
|
||||
ignoreFailures = false
|
||||
enableExperimentalRules = true
|
||||
filter {
|
||||
exclude("**/generated/**")
|
||||
include("**/kotlin/**")
|
||||
include("**/java/**")
|
||||
}
|
||||
|
||||
// Configure max line length
|
||||
kotlinScriptAdditionalPaths {
|
||||
include(fileTree("scripts/"))
|
||||
}
|
||||
|
||||
// Set max line length to 160
|
||||
version.set("0.50.0")
|
||||
}
|
||||
|
||||
task lintFixAll {
|
||||
description = "Run all auto-fixers (ktlint and Android lint)"
|
||||
group = "formatting"
|
||||
dependsOn ktlintFormat, lintFix
|
||||
}
|
||||
|
||||
// Add a task dependency to make ktlint run before build
|
||||
tasks.named("preBuild") {
|
||||
dependsOn("ktlintCheck")
|
||||
}
|
||||
|
||||
// Ensure codegen completes before ktlint tasks run
|
||||
afterEvaluate {
|
||||
tasks.withType(org.jlleitschuh.gradle.ktlint.tasks.KtLintCheckTask).configureEach {
|
||||
dependsOn("generateCodegenArtifactsFromSchema")
|
||||
}
|
||||
tasks.withType(org.jlleitschuh.gradle.ktlint.tasks.KtLintFormatTask).configureEach {
|
||||
dependsOn("generateCodegenArtifactsFromSchema")
|
||||
}
|
||||
}
|
||||
|
||||
19
apps/mobile-app/android/app/lint.xml
Normal file
19
apps/mobile-app/android/app/lint.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<!-- Disable some rules that are too strict or not relevant -->
|
||||
<issue id="ObsoleteLintCustomCheck" severity="ignore" />
|
||||
<issue id="GradleDependency" severity="ignore" />
|
||||
|
||||
<!-- Enable and configure important rules -->
|
||||
<issue id="NewApi" severity="error" />
|
||||
<issue id="InlinedApi" severity="error" />
|
||||
<issue id="MissingPermission" severity="error" />
|
||||
<issue id="HardcodedText" severity="warning" />
|
||||
<issue id="UnusedResources" severity="warning" />
|
||||
<issue id="ContentDescription" severity="warning" />
|
||||
<issue id="ClickableViewAccessibility" severity="warning" />
|
||||
|
||||
<!-- Kotlin specific rules -->
|
||||
<issue id="KotlinPropertyAccess" severity="warning" />
|
||||
<issue id="KotlinNullness" severity="error" />
|
||||
</lint>
|
||||
@@ -1,52 +1,44 @@
|
||||
package net.aliasvault.app
|
||||
import expo.modules.splashscreen.SplashScreenManager
|
||||
|
||||
import android.os.Bundle
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager
|
||||
import android.graphics.Color
|
||||
import android.view.Window
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
import expo.modules.splashscreen.SplashScreenManager
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
// setTheme(R.style.AppTheme);
|
||||
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
|
||||
SplashScreenManager.registerOnActivity(this)
|
||||
// @generated end expo-splashscreen
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
// setTheme(R.style.AppTheme);
|
||||
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
|
||||
SplashScreenManager.registerOnActivity(this)
|
||||
// @generated end expo-splashscreen
|
||||
|
||||
super.onCreate(null)
|
||||
}
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this,
|
||||
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
object : DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName,
|
||||
fabricEnabled
|
||||
){})
|
||||
}
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this,
|
||||
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
object : DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName,
|
||||
fabricEnabled,
|
||||
) {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,58 +2,56 @@ package net.aliasvault.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
import net.aliasvault.app.nativevaultmanager.NativeVaultManagerPackage
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
import expo.modules.ReactNativeHostWrapper
|
||||
import net.aliasvault.app.nativevaultmanager.NativeVaultManagerPackage
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
||||
this,
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> {
|
||||
val packages = PackageList(this).packages
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
packages.add(NativeVaultManagerPackage())
|
||||
return packages
|
||||
}
|
||||
override fun getPackages(): List<ReactPackage> {
|
||||
val packages = PackageList(this).packages
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
packages.add(NativeVaultManagerPackage())
|
||||
return packages
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
)
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
},
|
||||
)
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
|
||||
override val reactHost: ReactHost
|
||||
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
package net.aliasvault.app.autofill
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.CancellationSignal
|
||||
import android.service.autofill.AutofillService
|
||||
@@ -20,14 +21,15 @@ import android.service.autofill.SaveRequest
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import net.aliasvault.app.MainActivity
|
||||
import net.aliasvault.app.R
|
||||
import net.aliasvault.app.autofill.models.FieldType
|
||||
import net.aliasvault.app.autofill.utils.CredentialMatcher
|
||||
import net.aliasvault.app.autofill.utils.FieldFinder
|
||||
import net.aliasvault.app.autofill.utils.ImageUtils
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.VaultStore.CredentialOperationCallback
|
||||
import net.aliasvault.app.vaultstore.models.Credential
|
||||
import android.app.PendingIntent
|
||||
import net.aliasvault.app.MainActivity
|
||||
import net.aliasvault.app.R
|
||||
import net.aliasvault.app.autofill.utils.*
|
||||
import net.aliasvault.app.autofill.models.FieldType
|
||||
|
||||
class AutofillService : AutofillService() {
|
||||
private val TAG = "AliasVaultAutofill"
|
||||
@@ -36,7 +38,7 @@ class AutofillService : AutofillService() {
|
||||
override fun onFillRequest(
|
||||
request: FillRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback
|
||||
callback: FillCallback,
|
||||
) {
|
||||
Log.d(TAG, "onFillRequest called")
|
||||
|
||||
@@ -119,19 +121,26 @@ class AutofillService : AutofillService() {
|
||||
result
|
||||
}
|
||||
|
||||
Log.d(TAG, "Amount of credentials filtered with this app info: ${filteredCredentials.size}")
|
||||
Log.d(
|
||||
TAG,
|
||||
"Amount of credentials filtered with this app info: ${filteredCredentials.size}",
|
||||
)
|
||||
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
|
||||
// If there are no results, return "no matches" placeholder option.
|
||||
if (filteredCredentials.isEmpty()) {
|
||||
Log.d(TAG, "No credentials found for this app, showing 'no matches' option")
|
||||
Log.d(
|
||||
TAG,
|
||||
"No credentials found for this app, showing 'no matches' option",
|
||||
)
|
||||
responseBuilder.addDataset(createNoMatchesDataset(fieldFinder))
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// If there are matches, add them to the dataset
|
||||
for (credential in filteredCredentials) {
|
||||
responseBuilder.addDataset(createCredentialDataset(fieldFinder, credential))
|
||||
responseBuilder.addDataset(
|
||||
createCredentialDataset(fieldFinder, credential),
|
||||
)
|
||||
}
|
||||
|
||||
// Add "Open AliasVault app" as the last option
|
||||
@@ -149,7 +158,8 @@ class AutofillService : AutofillService() {
|
||||
Log.e(TAG, "Error getting credentials", e)
|
||||
callback.onSuccess(null)
|
||||
}
|
||||
})) {
|
||||
})
|
||||
) {
|
||||
// Successfully used cached key - method returns true
|
||||
Log.d(TAG, "Successfully retrieved credentials with unlocked vault")
|
||||
return
|
||||
@@ -166,10 +176,7 @@ class AutofillService : AutofillService() {
|
||||
}
|
||||
|
||||
// Helper method to create a dataset from a credential
|
||||
private fun createCredentialDataset(
|
||||
fieldFinder: FieldFinder,
|
||||
credential: Credential
|
||||
) : Dataset {
|
||||
private fun createCredentialDataset(fieldFinder: FieldFinder, credential: Credential): Dataset {
|
||||
// Choose layout based on whether we have a logo
|
||||
val layoutId = if (credential.service.logo != null) {
|
||||
R.layout.autofill_dataset_item_icon
|
||||
@@ -189,34 +196,55 @@ class AutofillService : AutofillService() {
|
||||
when (fieldType) {
|
||||
FieldType.PASSWORD -> {
|
||||
if (credential.password != null) {
|
||||
dataSetBuilder.setValue(field.first, AutofillValue.forText(credential.password.value as CharSequence))
|
||||
dataSetBuilder.setValue(
|
||||
field.first,
|
||||
AutofillValue.forText(credential.password.value as CharSequence),
|
||||
)
|
||||
}
|
||||
}
|
||||
FieldType.EMAIL -> {
|
||||
if (credential.alias?.email != null) {
|
||||
dataSetBuilder.setValue(field.first, AutofillValue.forText(credential.alias.email))
|
||||
dataSetBuilder.setValue(
|
||||
field.first,
|
||||
AutofillValue.forText(credential.alias.email),
|
||||
)
|
||||
presentationDisplayValue = "${credential.service.name} (${credential.alias.email})"
|
||||
} else if (credential.username != null) {
|
||||
dataSetBuilder.setValue(field.first, AutofillValue.forText(credential.username))
|
||||
dataSetBuilder.setValue(
|
||||
field.first,
|
||||
AutofillValue.forText(credential.username),
|
||||
)
|
||||
presentationDisplayValue = "${credential.service.name} (${credential.username})"
|
||||
}
|
||||
}
|
||||
FieldType.USERNAME -> {
|
||||
if (credential.username != null) {
|
||||
dataSetBuilder.setValue(field.first, AutofillValue.forText(credential.username))
|
||||
dataSetBuilder.setValue(
|
||||
field.first,
|
||||
AutofillValue.forText(credential.username),
|
||||
)
|
||||
presentationDisplayValue = "${credential.service.name} (${credential.username})"
|
||||
} else if (credential.alias?.email != null) {
|
||||
dataSetBuilder.setValue(field.first, AutofillValue.forText(credential.alias.email))
|
||||
dataSetBuilder.setValue(
|
||||
field.first,
|
||||
AutofillValue.forText(credential.alias.email),
|
||||
)
|
||||
presentationDisplayValue = "${credential.service.name} (${credential.alias.email})"
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// For unknown field types, try both email and username
|
||||
if (credential.alias?.email != null) {
|
||||
dataSetBuilder.setValue(field.first, AutofillValue.forText(credential.alias.email))
|
||||
dataSetBuilder.setValue(
|
||||
field.first,
|
||||
AutofillValue.forText(credential.alias.email),
|
||||
)
|
||||
presentationDisplayValue = "${credential.service.name} (${credential.alias.email})"
|
||||
} else if (credential.username != null) {
|
||||
dataSetBuilder.setValue(field.first, AutofillValue.forText(credential.username))
|
||||
dataSetBuilder.setValue(
|
||||
field.first,
|
||||
AutofillValue.forText(credential.username),
|
||||
)
|
||||
presentationDisplayValue = "${credential.service.name} (${credential.username})"
|
||||
}
|
||||
}
|
||||
@@ -226,7 +254,7 @@ class AutofillService : AutofillService() {
|
||||
// Set the display value of the dropdown item.
|
||||
presentation.setTextViewText(
|
||||
R.id.text,
|
||||
presentationDisplayValue
|
||||
presentationDisplayValue,
|
||||
)
|
||||
|
||||
// Set the logo if available
|
||||
@@ -246,7 +274,7 @@ class AutofillService : AutofillService() {
|
||||
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
presentation.setTextViewText(
|
||||
R.id.text,
|
||||
"No match found, create new?"
|
||||
"No match found, create new?",
|
||||
)
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
@@ -267,7 +295,7 @@ class AutofillService : AutofillService() {
|
||||
this@AutofillService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
dataSetBuilder.setAuthentication(pendingIntent.intentSender)
|
||||
|
||||
@@ -285,7 +313,7 @@ class AutofillService : AutofillService() {
|
||||
val openAppPresentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
openAppPresentation.setTextViewText(
|
||||
R.id.text,
|
||||
"Open app"
|
||||
"Open app",
|
||||
)
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(openAppPresentation)
|
||||
@@ -306,7 +334,7 @@ class AutofillService : AutofillService() {
|
||||
this@AutofillService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
dataSetBuilder.setAuthentication(pendingIntent.intentSender)
|
||||
|
||||
@@ -325,7 +353,7 @@ class AutofillService : AutofillService() {
|
||||
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
presentation.setTextViewText(
|
||||
R.id.text,
|
||||
"Vault locked"
|
||||
"Vault locked",
|
||||
)
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
@@ -339,7 +367,7 @@ class AutofillService : AutofillService() {
|
||||
this@AutofillService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
dataSetBuilder.setAuthentication(pendingIntent.intentSender)
|
||||
|
||||
@@ -353,7 +381,10 @@ class AutofillService : AutofillService() {
|
||||
return dataSetBuilder.build()
|
||||
}
|
||||
|
||||
private fun filterCredentialsByAppInfo(credentials: List<Credential>, appInfo: String): List<Credential> {
|
||||
private fun filterCredentialsByAppInfo(
|
||||
credentials: List<Credential>,
|
||||
appInfo: String,
|
||||
): List<Credential> {
|
||||
return credentialMatcher.filterCredentialsByAppInfo(credentials, appInfo)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@ enum class FieldType {
|
||||
EMAIL,
|
||||
USERNAME,
|
||||
PASSWORD,
|
||||
UNKNOWN
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ class CredentialMatcher {
|
||||
* 4. Domain key match (package middle segment or domain without TLD)
|
||||
* 5. General text matching on service name, username, and URL
|
||||
*/
|
||||
fun filterCredentialsByAppInfo(credentials: List<Credential>, appInfo: String): List<Credential> {
|
||||
fun filterCredentialsByAppInfo(
|
||||
credentials: List<Credential>,
|
||||
appInfo: String,
|
||||
): List<Credential> {
|
||||
if (appInfo.isBlank()) return credentials
|
||||
|
||||
val input = appInfo.trim().lowercase()
|
||||
@@ -27,7 +30,12 @@ class CredentialMatcher {
|
||||
val rootDomain: String?
|
||||
val domainKey: String
|
||||
|
||||
if (isUrlLike && (input.startsWith("http://") || input.startsWith("https://") || input.startsWith("www."))) {
|
||||
if (isUrlLike && (
|
||||
input.startsWith("http://") || input.startsWith("https://") || input.startsWith(
|
||||
"www.",
|
||||
)
|
||||
)
|
||||
) {
|
||||
// Treat as full or partial URL
|
||||
val cleaned = input
|
||||
.removePrefix("https://")
|
||||
@@ -57,7 +65,7 @@ class CredentialMatcher {
|
||||
cred.service.url?.trim()?.lowercase() in listOf(
|
||||
input,
|
||||
"https://$host",
|
||||
"http://$host"
|
||||
"http://$host",
|
||||
)
|
||||
}
|
||||
// 2. Base URL match
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.aliasvault.app.autofill.utils
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.autofill.AutofillId
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import net.aliasvault.app.autofill.models.FieldType
|
||||
|
||||
@@ -126,7 +126,8 @@ class FieldFinder(var structure: AssistStructure) {
|
||||
for (hint in hints) {
|
||||
if (hint.contains("web", ignoreCase = true) ||
|
||||
hint.contains("url", ignoreCase = true) ||
|
||||
hint.contains("domain", ignoreCase = true)) {
|
||||
hint.contains("domain", ignoreCase = true)
|
||||
) {
|
||||
return hint
|
||||
}
|
||||
}
|
||||
@@ -208,7 +209,8 @@ class FieldFinder(var structure: AssistStructure) {
|
||||
val idEntry = node.idEntry
|
||||
val hint = node.hint
|
||||
if (idEntry?.contains("email", ignoreCase = true) == true ||
|
||||
hint?.contains("email", ignoreCase = true) == true) {
|
||||
hint?.contains("email", ignoreCase = true) == true
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -221,7 +223,8 @@ class FieldFinder(var structure: AssistStructure) {
|
||||
if (hints != null) {
|
||||
for (hint in hints) {
|
||||
if (hint == View.AUTOFILL_HINT_USERNAME ||
|
||||
hint.contains("username", ignoreCase = true)) {
|
||||
hint.contains("username", ignoreCase = true)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -248,7 +251,8 @@ class FieldFinder(var structure: AssistStructure) {
|
||||
if (idEntry?.contains("username", ignoreCase = true) == true ||
|
||||
idEntry?.contains("user", ignoreCase = true) == true ||
|
||||
hint?.contains("username", ignoreCase = true) == true ||
|
||||
hint?.contains("user", ignoreCase = true) == true) {
|
||||
hint?.contains("user", ignoreCase = true) == true
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -268,7 +272,11 @@ class FieldFinder(var structure: AssistStructure) {
|
||||
val hints = node.autofillHints
|
||||
if (hints != null) {
|
||||
for (hint in hints) {
|
||||
if (hint == View.AUTOFILL_HINT_PASSWORD || hint.contains("password", ignoreCase = true)) {
|
||||
if (hint == View.AUTOFILL_HINT_PASSWORD || hint.contains(
|
||||
"password",
|
||||
ignoreCase = true,
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -276,12 +284,18 @@ class FieldFinder(var structure: AssistStructure) {
|
||||
|
||||
// Check by input type
|
||||
val inputType = node.inputType
|
||||
val isPasswordType = (inputType and android.text.InputType.TYPE_MASK_CLASS == android.text.InputType.TYPE_CLASS_TEXT &&
|
||||
(inputType and android.text.InputType.TYPE_MASK_VARIATION == android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD ||
|
||||
inputType and android.text.InputType.TYPE_MASK_VARIATION == android.text.InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ||
|
||||
inputType and android.text.InputType.TYPE_MASK_VARIATION == android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)) ||
|
||||
(inputType and android.text.InputType.TYPE_MASK_CLASS == android.text.InputType.TYPE_CLASS_NUMBER &&
|
||||
inputType and android.text.InputType.TYPE_MASK_VARIATION == android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD)
|
||||
val isPasswordType = (
|
||||
inputType and android.text.InputType.TYPE_MASK_CLASS == android.text.InputType.TYPE_CLASS_TEXT &&
|
||||
(
|
||||
inputType and android.text.InputType.TYPE_MASK_VARIATION == android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD ||
|
||||
inputType and android.text.InputType.TYPE_MASK_VARIATION == android.text.InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ||
|
||||
inputType and android.text.InputType.TYPE_MASK_VARIATION == android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
)
|
||||
) ||
|
||||
(
|
||||
inputType and android.text.InputType.TYPE_MASK_CLASS == android.text.InputType.TYPE_CLASS_NUMBER &&
|
||||
inputType and android.text.InputType.TYPE_MASK_VARIATION == android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||
)
|
||||
if (isPasswordType) {
|
||||
return true
|
||||
}
|
||||
@@ -323,7 +337,10 @@ class FieldFinder(var structure: AssistStructure) {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findNodeByIdRecursive(node: AssistStructure.ViewNode, fieldId: AutofillId): AssistStructure.ViewNode? {
|
||||
private fun findNodeByIdRecursive(
|
||||
node: AssistStructure.ViewNode,
|
||||
fieldId: AutofillId,
|
||||
): AssistStructure.ViewNode? {
|
||||
if (node.autofillId == fieldId) {
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package net.aliasvault.app.autofill.utils
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.content.res.Resources
|
||||
import com.caverock.androidsvg.SVG
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
@@ -88,6 +88,5 @@ object ImageUtils {
|
||||
}
|
||||
|
||||
fun base64ToBytes(base64: String): ByteArray? =
|
||||
try { Base64.decode(base64, Base64.DEFAULT) }
|
||||
catch (e: Exception) { null }
|
||||
try { Base64.decode(base64, Base64.DEFAULT) } catch (e: Exception) { null }
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
package net.aliasvault.app.nativevaultmanager
|
||||
|
||||
import com.facebook.react.module.annotations.ReactModule
|
||||
import com.facebook.react.turbomodule.core.interfaces.TurboModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.aliasvault.nativevaultmanager.NativeVaultManagerSpec
|
||||
import com.facebook.react.bridge.*
|
||||
import org.json.JSONArray
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.LifecycleEventListener
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableType
|
||||
import com.facebook.react.module.annotations.ReactModule
|
||||
import com.facebook.react.turbomodule.core.interfaces.TurboModule
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.core.net.toUri
|
||||
import net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider
|
||||
import org.json.JSONArray
|
||||
|
||||
@ReactModule(name = NativeVaultManager.NAME)
|
||||
class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
@@ -28,7 +31,7 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
|
||||
private val vaultStore = VaultStore.getInstance(
|
||||
AndroidKeystoreProvider(reactContext) { getFragmentActivity() },
|
||||
AndroidStorageProvider(reactContext)
|
||||
AndroidStorageProvider(reactContext),
|
||||
)
|
||||
|
||||
init {
|
||||
@@ -159,7 +162,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error storing key derivation params", e)
|
||||
promise.reject("ERR_STORE_KEY_PARAMS", "Failed to store key derivation params: ${e.message}", e)
|
||||
promise.reject(
|
||||
"ERR_STORE_KEY_PARAMS",
|
||||
"Failed to store key derivation params: ${e.message}",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +177,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
promise.resolve(params)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting key derivation params", e)
|
||||
promise.reject("ERR_GET_KEY_PARAMS", "Failed to get key derivation params: ${e.message}", e)
|
||||
promise.reject(
|
||||
"ERR_GET_KEY_PARAMS",
|
||||
"Failed to get key derivation params: ${e.message}",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +257,10 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
is Float -> rowMap.putDouble(key, value.toDouble())
|
||||
is Double -> rowMap.putDouble(key, value)
|
||||
is String -> rowMap.putString(key, value)
|
||||
is ByteArray -> rowMap.putString(key, android.util.Base64.encodeToString(value, android.util.Base64.NO_WRAP))
|
||||
is ByteArray -> rowMap.putString(
|
||||
key,
|
||||
android.util.Base64.encodeToString(value, android.util.Base64.NO_WRAP),
|
||||
)
|
||||
else -> rowMap.putString(key, value.toString())
|
||||
}
|
||||
}
|
||||
@@ -299,7 +313,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error committing transaction", e)
|
||||
promise.reject("ERR_COMMIT_TRANSACTION", "Failed to commit transaction: ${e.message}", e)
|
||||
promise.reject(
|
||||
"ERR_COMMIT_TRANSACTION",
|
||||
"Failed to commit transaction: ${e.message}",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +328,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error rolling back transaction", e)
|
||||
promise.reject("ERR_ROLLBACK_TRANSACTION", "Failed to rollback transaction: ${e.message}", e)
|
||||
promise.reject(
|
||||
"ERR_ROLLBACK_TRANSACTION",
|
||||
"Failed to rollback transaction: ${e.message}",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,10 +396,13 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
}
|
||||
|
||||
promise.resolve(null)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error opening autofill settings", e)
|
||||
promise.reject("ERR_OPEN_AUTOFILL_SETTINGS", "Failed to open autofill settings: ${e.message}", e)
|
||||
promise.reject(
|
||||
"ERR_OPEN_AUTOFILL_SETTINGS",
|
||||
"Failed to open autofill settings: ${e.message}",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ class NativeVaultManagerPackage : TurboReactPackage() {
|
||||
NativeVaultManager.NAME,
|
||||
NativeVaultManager::class.java.name,
|
||||
false, // canOverrideExistingModule
|
||||
true, // needsEagerInit
|
||||
true, // hasConstants
|
||||
true, // needsEagerInit
|
||||
true, // hasConstants
|
||||
false, // isCxxModule
|
||||
true // isTurboModule
|
||||
true, // isTurboModule
|
||||
)
|
||||
moduleMap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,27 @@ package net.aliasvault.app.vaultstore
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteException
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import net.aliasvault.app.vaultstore.models.*
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.KeystoreOperationCallback
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.KeystoreProvider
|
||||
import net.aliasvault.app.vaultstore.models.Alias
|
||||
import net.aliasvault.app.vaultstore.models.Credential
|
||||
import net.aliasvault.app.vaultstore.models.Password
|
||||
import net.aliasvault.app.vaultstore.models.Service
|
||||
import net.aliasvault.app.vaultstore.models.VaultMetadata
|
||||
import net.aliasvault.app.vaultstore.storageprovider.StorageProvider
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import java.security.SecureRandom
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import net.aliasvault.app.vaultstore.storageprovider.StorageProvider
|
||||
import org.json.JSONArray
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.KeystoreProvider
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.KeystoreOperationCallback
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
class VaultStore(
|
||||
private val storageProvider: StorageProvider,
|
||||
@@ -72,7 +76,7 @@ class VaultStore(
|
||||
error = e
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Wait for the callback to complete
|
||||
@@ -111,7 +115,7 @@ class VaultStore(
|
||||
Log.e(TAG, "Error retrieving key", e)
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
callback.onError(Exception("No encryption key found"))
|
||||
@@ -136,7 +140,7 @@ class VaultStore(
|
||||
* Get the encrypted database from the storage provider
|
||||
* @return The encrypted database as a base64 encoded string
|
||||
*/
|
||||
fun getEncryptedDatabase() : String {
|
||||
fun getEncryptedDatabase(): String {
|
||||
val encryptedDbBase64 = storageProvider.getEncryptedDatabaseFile().readText()
|
||||
return encryptedDbBase64
|
||||
}
|
||||
@@ -145,7 +149,7 @@ class VaultStore(
|
||||
* Check if the encrypted database exists in the storage provider
|
||||
* @return True if the encrypted database exists, false otherwise
|
||||
*/
|
||||
fun hasEncryptedDatabase() : Boolean {
|
||||
fun hasEncryptedDatabase(): Boolean {
|
||||
return storageProvider.getEncryptedDatabaseFile().exists()
|
||||
}
|
||||
|
||||
@@ -161,7 +165,7 @@ class VaultStore(
|
||||
* Get the metadata from the storage provider
|
||||
* @return The metadata as a string
|
||||
*/
|
||||
fun getMetadata() : String {
|
||||
fun getMetadata(): String {
|
||||
return storageProvider.getMetadata()
|
||||
}
|
||||
|
||||
@@ -200,10 +204,18 @@ class VaultStore(
|
||||
for (columnName in columnNames) {
|
||||
when (it.getType(it.getColumnIndexOrThrow(columnName))) {
|
||||
android.database.Cursor.FIELD_TYPE_NULL -> row[columnName] = null
|
||||
android.database.Cursor.FIELD_TYPE_INTEGER -> row[columnName] = it.getLong(it.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_FLOAT -> row[columnName] = it.getDouble(it.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_STRING -> row[columnName] = it.getString(it.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_BLOB -> row[columnName] = it.getBlob(it.getColumnIndexOrThrow(columnName))
|
||||
android.database.Cursor.FIELD_TYPE_INTEGER -> row[columnName] = it.getLong(
|
||||
it.getColumnIndexOrThrow(columnName),
|
||||
)
|
||||
android.database.Cursor.FIELD_TYPE_FLOAT -> row[columnName] = it.getDouble(
|
||||
it.getColumnIndexOrThrow(columnName),
|
||||
)
|
||||
android.database.Cursor.FIELD_TYPE_STRING -> row[columnName] = it.getString(
|
||||
it.getColumnIndexOrThrow(columnName),
|
||||
)
|
||||
android.database.Cursor.FIELD_TYPE_BLOB -> row[columnName] = it.getBlob(
|
||||
it.getColumnIndexOrThrow(columnName),
|
||||
)
|
||||
}
|
||||
}
|
||||
results.add(row)
|
||||
@@ -261,14 +273,16 @@ class VaultStore(
|
||||
// Get all table names from the main database
|
||||
val cursor = dbConnection?.rawQuery(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'android_%'",
|
||||
null
|
||||
null,
|
||||
)
|
||||
|
||||
cursor?.use {
|
||||
while (it.moveToNext()) {
|
||||
val tableName = it.getString(0)
|
||||
// Create table and copy data
|
||||
dbConnection?.execSQL("CREATE TABLE target.$tableName AS SELECT * FROM main.$tableName")
|
||||
dbConnection?.execSQL(
|
||||
"CREATE TABLE target.$tableName AS SELECT * FROM main.$tableName",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,8 +310,7 @@ class VaultStore(
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error exporting and encrypting database", e)
|
||||
throw e
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
// Remove the temporary file
|
||||
tempDbFile.delete()
|
||||
}
|
||||
@@ -339,11 +352,13 @@ class VaultStore(
|
||||
fun setVaultRevisionNumber(revisionNumber: Int) {
|
||||
val metadata = getVaultMetadataObject() ?: VaultMetadata()
|
||||
val updatedMetadata = metadata.copy(vaultRevisionNumber = revisionNumber)
|
||||
storeMetadata(JSONObject().apply {
|
||||
put("publicEmailDomains", JSONArray(updatedMetadata.publicEmailDomains))
|
||||
put("privateEmailDomains", JSONArray(updatedMetadata.privateEmailDomains))
|
||||
put("vaultRevisionNumber", updatedMetadata.vaultRevisionNumber)
|
||||
}.toString())
|
||||
storeMetadata(
|
||||
JSONObject().apply {
|
||||
put("publicEmailDomains", JSONArray(updatedMetadata.publicEmailDomains))
|
||||
put("privateEmailDomains", JSONArray(updatedMetadata.privateEmailDomains))
|
||||
put("vaultRevisionNumber", updatedMetadata.vaultRevisionNumber)
|
||||
}.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
fun getVaultRevisionNumber(): Int {
|
||||
@@ -364,7 +379,7 @@ class VaultStore(
|
||||
privateEmailDomains = json.optJSONArray("privateEmailDomains")?.let { array ->
|
||||
List(array.length()) { i -> array.getString(i) }
|
||||
} ?: emptyList(),
|
||||
vaultRevisionNumber = json.optInt("vaultRevisionNumber", 0)
|
||||
vaultRevisionNumber = json.optInt("vaultRevisionNumber", 0),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error parsing vault metadata", e)
|
||||
@@ -528,7 +543,7 @@ class VaultStore(
|
||||
// Verify the attachment worked by checking if we can access the source database
|
||||
val verifyCursor = dbConnection?.rawQuery(
|
||||
"SELECT name FROM source.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
||||
null
|
||||
null,
|
||||
)
|
||||
|
||||
if (verifyCursor == null) {
|
||||
@@ -543,7 +558,9 @@ class VaultStore(
|
||||
do {
|
||||
val tableName = it.getString(0)
|
||||
// Create table and copy data using rawQuery
|
||||
dbConnection?.execSQL("CREATE TABLE $tableName AS SELECT * FROM source.$tableName")
|
||||
dbConnection?.execSQL(
|
||||
"CREATE TABLE $tableName AS SELECT * FROM source.$tableName",
|
||||
)
|
||||
} while (it.moveToNext())
|
||||
}
|
||||
|
||||
@@ -562,7 +579,6 @@ class VaultStore(
|
||||
dbConnection?.rawQuery("PRAGMA foreign_keys = ON", null)
|
||||
|
||||
lastUnlockTime = System.currentTimeMillis()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error setting up database with decrypted data", e)
|
||||
throw e
|
||||
@@ -648,7 +664,7 @@ class VaultStore(
|
||||
logo = it.getBlob(10),
|
||||
createdAt = parseDateString(it.getString(11)) ?: MIN_DATE,
|
||||
updatedAt = parseDateString(it.getString(12)) ?: MIN_DATE,
|
||||
isDeleted = it.getInt(13) == 1
|
||||
isDeleted = it.getInt(13) == 1,
|
||||
)
|
||||
|
||||
// Password
|
||||
@@ -660,7 +676,7 @@ class VaultStore(
|
||||
value = it.getString(15),
|
||||
createdAt = parseDateString(it.getString(16)) ?: MIN_DATE,
|
||||
updatedAt = parseDateString(it.getString(17)) ?: MIN_DATE,
|
||||
isDeleted = it.getInt(18) == 1
|
||||
isDeleted = it.getInt(18) == 1,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -677,7 +693,7 @@ class VaultStore(
|
||||
email = it.getString(25),
|
||||
createdAt = parseDateString(it.getString(26)) ?: MIN_DATE,
|
||||
updatedAt = parseDateString(it.getString(27)) ?: MIN_DATE,
|
||||
isDeleted = it.getInt(28) == 1
|
||||
isDeleted = it.getInt(28) == 1,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -690,7 +706,7 @@ class VaultStore(
|
||||
password = password,
|
||||
createdAt = parseDateString(it.getString(4)) ?: MIN_DATE,
|
||||
updatedAt = parseDateString(it.getString(5)) ?: MIN_DATE,
|
||||
isDeleted = isDeleted
|
||||
isDeleted = isDeleted,
|
||||
)
|
||||
result.add(credential)
|
||||
} catch (e: Exception) {
|
||||
@@ -720,7 +736,7 @@ class VaultStore(
|
||||
},
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
for (format in formats) {
|
||||
@@ -773,7 +789,10 @@ class VaultStore(
|
||||
private var instance: VaultStore? = null
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(keystoreProvider: KeystoreProvider, storageProvider: StorageProvider): VaultStore {
|
||||
fun getInstance(
|
||||
keystoreProvider: KeystoreProvider,
|
||||
storageProvider: StorageProvider,
|
||||
): VaultStore {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: VaultStore(storageProvider, keystoreProvider).also { instance = it }
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package net.aliasvault.app.vaultstore.keystoreprovider
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
@@ -26,7 +25,7 @@ import javax.crypto.spec.GCMParameterSpec
|
||||
*/
|
||||
class AndroidKeystoreProvider(
|
||||
private val context: Context,
|
||||
private val getCurrentActivity: () -> Activity?
|
||||
private val getCurrentActivity: () -> Activity?,
|
||||
) : KeystoreProvider {
|
||||
private val biometricManager = BiometricManager.from(context)
|
||||
private val executor: Executor = Executors.newSingleThreadExecutor()
|
||||
@@ -39,8 +38,8 @@ class AndroidKeystoreProvider(
|
||||
override fun isBiometricAvailable(): Boolean {
|
||||
return biometricManager.canAuthenticate(
|
||||
BiometricManager.Authenticators.BIOMETRIC_WEAK or
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL,
|
||||
) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
}
|
||||
|
||||
@@ -49,7 +48,9 @@ class AndroidKeystoreProvider(
|
||||
try {
|
||||
val currentActivity = getCurrentActivity()
|
||||
if (currentActivity == null || !(currentActivity is FragmentActivity)) {
|
||||
callback.onError(Exception("No activity available for biometric authentication"))
|
||||
callback.onError(
|
||||
Exception("No activity available for biometric authentication"),
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
@@ -60,12 +61,13 @@ class AndroidKeystoreProvider(
|
||||
// Create or get biometric key
|
||||
if (!keyStore.containsAlias(KEYSTORE_ALIAS)) {
|
||||
val keyGenerator = KeyGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
|
||||
KeyProperties.KEY_ALGORITHM_AES,
|
||||
"AndroidKeyStore",
|
||||
)
|
||||
|
||||
val keySpec = KeyGenParameterSpec.Builder(
|
||||
KEYSTORE_ALIAS,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
|
||||
)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
@@ -82,22 +84,28 @@ class AndroidKeystoreProvider(
|
||||
// Create BiometricPrompt
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle("Store Encryption Key")
|
||||
.setSubtitle("Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.")
|
||||
.setSubtitle(
|
||||
"Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.",
|
||||
)
|
||||
.setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL,
|
||||
)
|
||||
.build()
|
||||
|
||||
val biometricPrompt = BiometricPrompt(currentActivity, executor,
|
||||
val biometricPrompt = BiometricPrompt(
|
||||
currentActivity,
|
||||
executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult,
|
||||
) {
|
||||
try {
|
||||
// Initialize cipher for encryption
|
||||
val cipher = Cipher.getInstance(
|
||||
"${KeyProperties.KEY_ALGORITHM_AES}/" +
|
||||
"${KeyProperties.BLOCK_MODE_GCM}/" +
|
||||
KeyProperties.ENCRYPTION_PADDING_NONE
|
||||
"${KeyProperties.BLOCK_MODE_GCM}/" +
|
||||
KeyProperties.ENCRYPTION_PADDING_NONE,
|
||||
)
|
||||
|
||||
// Initialize cipher with the secret key
|
||||
@@ -114,31 +122,44 @@ class AndroidKeystoreProvider(
|
||||
val combined = byteBuffer.array()
|
||||
|
||||
// Store encrypted key in SharedPreferences
|
||||
val encryptedKeyB64 = Base64.encodeToString(combined, Base64.NO_WRAP)
|
||||
val prefs = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val encryptedKeyB64 = Base64.encodeToString(
|
||||
combined,
|
||||
Base64.NO_WRAP,
|
||||
)
|
||||
val prefs = context.getSharedPreferences(
|
||||
SHARED_PREFS_NAME,
|
||||
Context.MODE_PRIVATE,
|
||||
)
|
||||
prefs.edit().putString(ENCRYPTED_KEY_PREF, encryptedKeyB64).apply()
|
||||
|
||||
Log.d(TAG, "Encryption key stored successfully")
|
||||
callback.onSuccess("Key stored successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error storing encryption key", e)
|
||||
callback.onError(Exception("Failed to store encryption key: ${e.message}"))
|
||||
callback.onError(
|
||||
Exception("Failed to store encryption key: ${e.message}"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence,
|
||||
) {
|
||||
Log.e(TAG, "Authentication error: $errorCode - $errString")
|
||||
callback.onError(Exception("Authentication error: $errString (code: $errorCode)"))
|
||||
callback.onError(
|
||||
Exception("Authentication error: $errString (code: $errorCode)"),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
Log.e(TAG, "Authentication failed")
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// Show biometric prompt without crypto object for device credentials
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in biometric key storage", e)
|
||||
callback.onError(Exception("Failed to initialize key storage: ${e.message}"))
|
||||
@@ -151,7 +172,9 @@ class AndroidKeystoreProvider(
|
||||
try {
|
||||
val currentActivity = getCurrentActivity()
|
||||
if (currentActivity == null || !(currentActivity is FragmentActivity)) {
|
||||
callback.onError(Exception("No activity available for biometric authentication"))
|
||||
callback.onError(
|
||||
Exception("No activity available for biometric authentication"),
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
@@ -184,16 +207,22 @@ class AndroidKeystoreProvider(
|
||||
.setSubtitle("Authenticate to access your vault")
|
||||
.setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL,
|
||||
)
|
||||
.build()
|
||||
|
||||
val biometricPrompt = BiometricPrompt(currentActivity, executor,
|
||||
val biometricPrompt = BiometricPrompt(
|
||||
currentActivity,
|
||||
executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult,
|
||||
) {
|
||||
try {
|
||||
// Get the cipher from the result
|
||||
val cipher = result.cryptoObject?.cipher ?: throw Exception("Cipher is null")
|
||||
val cipher = result.cryptoObject?.cipher ?: throw Exception(
|
||||
"Cipher is null",
|
||||
)
|
||||
|
||||
// Decode combined data
|
||||
val combined = Base64.decode(encryptedKeyB64, Base64.NO_WRAP)
|
||||
@@ -220,7 +249,10 @@ class AndroidKeystoreProvider(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence,
|
||||
) {
|
||||
Log.e(TAG, "Authentication error: $errString")
|
||||
callback.onError(Exception("Authentication error: $errString"))
|
||||
}
|
||||
@@ -228,7 +260,8 @@ class AndroidKeystoreProvider(
|
||||
override fun onAuthenticationFailed() {
|
||||
Log.e(TAG, "Authentication failed")
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// Initialize cipher for decryption with IV from stored encrypted key
|
||||
val combined = Base64.decode(encryptedKeyB64, Base64.NO_WRAP)
|
||||
@@ -238,15 +271,14 @@ class AndroidKeystoreProvider(
|
||||
|
||||
val cipher = Cipher.getInstance(
|
||||
"${KeyProperties.KEY_ALGORITHM_AES}/" +
|
||||
"${KeyProperties.BLOCK_MODE_GCM}/" +
|
||||
KeyProperties.ENCRYPTION_PADDING_NONE
|
||||
"${KeyProperties.BLOCK_MODE_GCM}/" +
|
||||
KeyProperties.ENCRYPTION_PADDING_NONE,
|
||||
)
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
|
||||
// Show biometric prompt
|
||||
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in biometric key retrieval", e)
|
||||
callback.onError(e)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package net.aliasvault.app.vaultstore.keystoreprovider
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
/**
|
||||
* Interface for keystore providers that handle secure storage of encryption keys with biometric protection.
|
||||
* This allows for different implementations for real devices and testing.
|
||||
@@ -19,19 +17,14 @@ interface KeystoreProvider {
|
||||
* @param key The encryption key to store
|
||||
* @param callback The callback to handle the result
|
||||
*/
|
||||
fun storeKey(
|
||||
key: String,
|
||||
callback: KeystoreOperationCallback
|
||||
)
|
||||
fun storeKey(key: String, callback: KeystoreOperationCallback)
|
||||
|
||||
/**
|
||||
* Retrieve an encryption key using biometric authentication
|
||||
* @param activity The activity to show the biometric prompt on
|
||||
* @param callback The callback to handle the result
|
||||
*/
|
||||
fun retrieveKey(
|
||||
callback: KeystoreOperationCallback
|
||||
)
|
||||
fun retrieveKey(callback: KeystoreOperationCallback)
|
||||
|
||||
/**
|
||||
* Clear all stored keys
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package net.aliasvault.app.vaultstore.keystoreprovider
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
/**
|
||||
* Test implementation of the keystore provider that does nothing and always returns false for biometric availability.
|
||||
* This is used for testing when biometrics are not available.
|
||||
@@ -11,17 +9,12 @@ class TestKeystoreProvider : KeystoreProvider {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun storeKey(
|
||||
key: String,
|
||||
callback: KeystoreOperationCallback
|
||||
) {
|
||||
override fun storeKey(key: String, callback: KeystoreOperationCallback) {
|
||||
// Do nothing in test implementation
|
||||
callback.onSuccess("Key stored successfully (test)")
|
||||
}
|
||||
|
||||
override fun retrieveKey(
|
||||
callback: KeystoreOperationCallback
|
||||
) {
|
||||
override fun retrieveKey(callback: KeystoreOperationCallback) {
|
||||
// Do nothing in test implementation
|
||||
callback.onError(Exception("No key found (test)"))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ data class Credential(
|
||||
val password: Password?,
|
||||
val createdAt: Date,
|
||||
val updatedAt: Date,
|
||||
val isDeleted: Boolean
|
||||
val isDeleted: Boolean,
|
||||
)
|
||||
|
||||
data class Service(
|
||||
@@ -22,7 +22,7 @@ data class Service(
|
||||
val logo: ByteArray?,
|
||||
val createdAt: Date,
|
||||
val updatedAt: Date,
|
||||
val isDeleted: Boolean
|
||||
val isDeleted: Boolean,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
@@ -59,7 +59,7 @@ data class Password(
|
||||
val value: String,
|
||||
val createdAt: Date,
|
||||
val updatedAt: Date,
|
||||
val isDeleted: Boolean
|
||||
val isDeleted: Boolean,
|
||||
)
|
||||
|
||||
data class Alias(
|
||||
@@ -72,7 +72,7 @@ data class Alias(
|
||||
val email: String?,
|
||||
val createdAt: Date,
|
||||
val updatedAt: Date,
|
||||
val isDeleted: Boolean
|
||||
val isDeleted: Boolean,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
@@ -107,4 +107,4 @@ data class Alias(
|
||||
result = 31 * result + isDeleted.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ package net.aliasvault.app.vaultstore.models
|
||||
data class VaultMetadata(
|
||||
val publicEmailDomains: List<String> = emptyList(),
|
||||
val privateEmailDomains: List<String> = emptyList(),
|
||||
val vaultRevisionNumber: Int = 0
|
||||
)
|
||||
val vaultRevisionNumber: Int = 0,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package net.aliasvault.app.vaultstore.storageprovider
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
import androidx.core.content.edit
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* A file provider that returns the encrypted database file from the Android filesystem.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package net.aliasvault.app.vaultstore.storageprovider
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package net.aliasvault.app.nativevaultmanager
|
||||
|
||||
import net.aliasvault.app.autofill.utils.CredentialMatcher
|
||||
import net.aliasvault.app.vaultstore.models.Credential
|
||||
import net.aliasvault.app.vaultstore.models.Service
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import net.aliasvault.app.autofill.CredentialMatcher
|
||||
import net.aliasvault.app.vaultstore.models.Credential
|
||||
import net.aliasvault.app.vaultstore.models.Service
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import kotlin.test.*
|
||||
import kotlin.test.DefaultAsserter.assertEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
@@ -27,28 +29,28 @@ class AutofillTest {
|
||||
createTestCredential(
|
||||
"Gmail",
|
||||
"https://gmail.com",
|
||||
"user@gmail.com"
|
||||
"user@gmail.com",
|
||||
),
|
||||
createTestCredential(
|
||||
"Google",
|
||||
"https://google.com",
|
||||
"user@gmail.com"
|
||||
"user@gmail.com",
|
||||
),
|
||||
createTestCredential(
|
||||
"Coolblue",
|
||||
"https://www.coolblue.nl",
|
||||
"user@coolblue.nl"
|
||||
"user@coolblue.nl",
|
||||
),
|
||||
createTestCredential(
|
||||
"Amazon",
|
||||
"https://amazon.com",
|
||||
"user@amazon.com"
|
||||
"user@amazon.com",
|
||||
),
|
||||
createTestCredential(
|
||||
"Coolblue App",
|
||||
"com.coolblue.app",
|
||||
"user@coolblue.nl"
|
||||
)
|
||||
"user@coolblue.nl",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,7 +58,7 @@ class AutofillTest {
|
||||
fun testExactUrlMatch() {
|
||||
val matches = credentialMatcher.filterCredentialsByAppInfo(
|
||||
testCredentials,
|
||||
"www.coolblue.nl"
|
||||
"www.coolblue.nl",
|
||||
)
|
||||
|
||||
assertEquals(1, matches.size)
|
||||
@@ -67,7 +69,7 @@ class AutofillTest {
|
||||
fun testBaseUrlMatch() {
|
||||
val matches = credentialMatcher.filterCredentialsByAppInfo(
|
||||
testCredentials,
|
||||
"https://gmail.com/signin"
|
||||
"https://gmail.com/signin",
|
||||
)
|
||||
|
||||
assertEquals(1, matches.size)
|
||||
@@ -78,7 +80,7 @@ class AutofillTest {
|
||||
fun testRootDomainMatch() {
|
||||
val matches = credentialMatcher.filterCredentialsByAppInfo(
|
||||
testCredentials,
|
||||
"https://mail.google.com"
|
||||
"https://mail.google.com",
|
||||
)
|
||||
|
||||
assertEquals(1, matches.size)
|
||||
@@ -89,7 +91,7 @@ class AutofillTest {
|
||||
fun testDomainNamePartMatch() {
|
||||
val matches = credentialMatcher.filterCredentialsByAppInfo(
|
||||
testCredentials,
|
||||
"https://coolblue.be"
|
||||
"https://coolblue.be",
|
||||
)
|
||||
|
||||
assertEquals(2, matches.size)
|
||||
@@ -101,7 +103,7 @@ class AutofillTest {
|
||||
fun testPackageNameMatch() {
|
||||
val matches = credentialMatcher.filterCredentialsByAppInfo(
|
||||
testCredentials,
|
||||
"com.coolblue.app"
|
||||
"com.coolblue.app",
|
||||
)
|
||||
|
||||
assertEquals(2, matches.size)
|
||||
@@ -113,7 +115,7 @@ class AutofillTest {
|
||||
fun testNoMatches() {
|
||||
val matches = credentialMatcher.filterCredentialsByAppInfo(
|
||||
testCredentials,
|
||||
"https://nonexistent.com"
|
||||
"https://nonexistent.com",
|
||||
)
|
||||
|
||||
assertTrue(matches.isEmpty())
|
||||
@@ -123,7 +125,7 @@ class AutofillTest {
|
||||
fun testInvalidUrl() {
|
||||
val matches = credentialMatcher.filterCredentialsByAppInfo(
|
||||
testCredentials,
|
||||
"not a url"
|
||||
"not a url",
|
||||
)
|
||||
|
||||
assertTrue(matches.isEmpty())
|
||||
@@ -132,7 +134,7 @@ class AutofillTest {
|
||||
private fun createTestCredential(
|
||||
serviceName: String,
|
||||
serviceUrl: String,
|
||||
username: String
|
||||
username: String,
|
||||
): Credential {
|
||||
return Credential(
|
||||
id = UUID.randomUUID(),
|
||||
@@ -143,7 +145,7 @@ class AutofillTest {
|
||||
logo = null,
|
||||
createdAt = Date(),
|
||||
updatedAt = Date(),
|
||||
isDeleted = false
|
||||
isDeleted = false,
|
||||
),
|
||||
username = username,
|
||||
password = null,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package net.aliasvault.app.nativevaultmanager
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.TestKeystoreProvider
|
||||
import net.aliasvault.app.vaultstore.storageprovider.TestStorageProvider
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.storageprovider.TestStorageProvider
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.TestKeystoreProvider
|
||||
import kotlin.test.*
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
@@ -97,7 +100,10 @@ class VaultStoreTest {
|
||||
try {
|
||||
// Insert the setting using raw SQL with parameters
|
||||
val insertSql = "INSERT INTO Settings (Key, Value, CreatedAt, UpdatedAt, IsDeleted) VALUES (?, ?, ?, ?, ?)"
|
||||
val insertResult = vaultStore.executeUpdate(insertSql, arrayOf(testKey, testValue, "2025-01-01 00:00:00", "2025-01-01 00:00:00", 0))
|
||||
val insertResult = vaultStore.executeUpdate(
|
||||
insertSql,
|
||||
arrayOf(testKey, testValue, "2025-01-01 00:00:00", "2025-01-01 00:00:00", 0),
|
||||
)
|
||||
assertTrue(insertResult > 0, "Setting insertion should succeed")
|
||||
|
||||
// Verify the setting was inserted by querying it
|
||||
@@ -119,8 +125,10 @@ class VaultStoreTest {
|
||||
val querySql2 = "SELECT MigrationId FROM __EFMigrationsHistory"
|
||||
val results2 = vaultStore.executeQuery(querySql2, arrayOf<Any?>())
|
||||
|
||||
assertTrue(results2.isNotEmpty(), "Should get a result (migration history table contents)")
|
||||
|
||||
assertTrue(
|
||||
results2.isNotEmpty(),
|
||||
"Should get a result (migration history table contents)",
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
// If anything fails, rollback the transaction
|
||||
throw e
|
||||
|
||||
@@ -19,10 +19,12 @@ buildscript {
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||
classpath('org.jlleitschuh.gradle:ktlint-gradle:11.6.1')
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
apply plugin: "org.jlleitschuh.gradle.ktlint"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
@@ -40,3 +42,16 @@ allprojects {
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
// Configure ktlint
|
||||
ktlint {
|
||||
android = true
|
||||
verbose = true
|
||||
outputToConsole = true
|
||||
ignoreFailures = false
|
||||
enableExperimentalRules = true
|
||||
filter {
|
||||
exclude("**/generated/**")
|
||||
include("**/kotlin/**")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,4 +43,23 @@ In order to run the Android unit tests:
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
You can also open up the project in Android Studio, navigate to the `VaultStoreTest.kt` file and run/debug individual tests.
|
||||
You can also open up the project in Android Studio, navigate to the `VaultStoreTest.kt` file and run/debug individual tests.
|
||||
|
||||
## Linting
|
||||
Linting is ran automatically during normal Android app build. You can however also run the linting checks manually:
|
||||
|
||||
### Kotlin
|
||||
```bash
|
||||
./gradlew ktlintCheck
|
||||
```
|
||||
|
||||
### Java
|
||||
```bash
|
||||
./gradlew lint
|
||||
```
|
||||
|
||||
### Auto fix linting issues
|
||||
To automatically fix linting issues (where possible), run this command:
|
||||
```bash
|
||||
./gradlew lintFixAll
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user