Add android linting checks and integrate in build process (#846)

This commit is contained in:
Leendert de Borst
2025-05-28 16:43:05 +02:00
parent 1b07c5de9f
commit c7ab42e9f2
24 changed files with 541 additions and 268 deletions

View 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

View File

@@ -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")
}
}

View 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>

View File

@@ -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,
) {},
)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -7,5 +7,5 @@ enum class FieldType {
EMAIL,
USERNAME,
PASSWORD,
UNKNOWN
UNKNOWN,
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 }
}

View File

@@ -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,
)
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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 }
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)"))
}

View File

@@ -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
}
}
}

View File

@@ -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,
)

View File

@@ -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.

View File

@@ -1,6 +1,5 @@
package net.aliasvault.app.vaultstore.storageprovider
import android.content.Context
import java.io.File
/**

View 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,

View File

@@ -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

View File

@@ -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/**")
}
}

View File

@@ -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
```