Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
4bb3ab3
Bump version to 0.14.4-beta.5
ultraelectronica Jun 15, 2026
43a898b
Add autofill service and selection activity to Android manifest
ultraelectronica Jun 20, 2026
99aa6fe
Add password list navigation to vault explorer
ultraelectronica Jun 20, 2026
251cca0
Include password index in vault backup
ultraelectronica Jun 20, 2026
a5320c1
Add autofill entry point and password vault registration methods
ultraelectronica Jun 20, 2026
9f29d2d
Add AutofillCallbackHolder to manage autofill callbacks
ultraelectronica Jun 20, 2026
13bda93
Add AutofillSelectionActivity for credential selection UI
ultraelectronica Jun 20, 2026
2911442
Add autofill service for credential management
ultraelectronica Jun 20, 2026
c267955
Add autofill suggestion layout XML
ultraelectronica Jun 20, 2026
5bbe7a2
Add Android autofill service metadata resource
ultraelectronica Jun 20, 2026
45d3763
Add PasswordEntry and PasswordContent models
ultraelectronica Jun 20, 2026
3a60ab3
Add password management providers with Riverpod
ultraelectronica Jun 20, 2026
b3e80f1
Add autofill credential selection screen
ultraelectronica Jun 20, 2026
40d403b
Add password editor screen
ultraelectronica Jun 20, 2026
8cfbe97
Add password list screen with search, filtering, and multi-select
ultraelectronica Jun 20, 2026
18d4f01
Add password service with encryption and vault integration
ultraelectronica Jun 20, 2026
7117215
Add clipboard utility with auto-clear functionality
ultraelectronica Jun 20, 2026
ce69e17
Add password entry card widget
ultraelectronica Jun 20, 2026
1c766fe
Add password generator sheet widget
ultraelectronica Jun 20, 2026
9d70019
Add AutofillApp entry point widget
ultraelectronica Jun 20, 2026
a386a3d
Remove dot and row wrapper from Active status indicator
ultraelectronica Jun 20, 2026
5ba2cde
Disable app backup and add network security config
ultraelectronica Jun 20, 2026
fe33d18
Remove TODO comment and enable ProGuard minification for release builds
ultraelectronica Jun 20, 2026
f9f0059
Remove compression usage example file
ultraelectronica Jun 20, 2026
41b1063
Remove redundant file existence checks in album covers
ultraelectronica Jun 20, 2026
bc267eb
Check biometric availability before authenticating in autofill
ultraelectronica Jun 20, 2026
ee7c364
Re-wrap encryption key after password/PIN change
ultraelectronica Jun 20, 2026
4340e54
Update KDF iteration options
ultraelectronica Jun 20, 2026
d5fc540
Remove redundant file existence check in thumbnail builder
ultraelectronica Jun 20, 2026
979a965
Remove redundant file existence check in folder cover image
ultraelectronica Jun 20, 2026
c75ee8f
Add tooltips, fix image loading, and improve lifecycle safety
ultraelectronica Jun 20, 2026
6d9972a
Add tooltips to media viewer buttons
ultraelectronica Jun 20, 2026
8a31b59
Replace GestureDetector with IconButton and fix folder selection logic
ultraelectronica Jun 20, 2026
89debd7
Dispose controllers after showing add note dialog
ultraelectronica Jun 20, 2026
2f52ead
Add autofill hints and tooltips to password editor form
ultraelectronica Jun 20, 2026
19aed75
Replace GestureDetector with IconButton for autofill dismiss
ultraelectronica Jun 20, 2026
874ebb2
Add tooltips to song player screen buttons
ultraelectronica Jun 20, 2026
8c6ecc8
Remove redundant file existence check in image thumbnail
ultraelectronica Jun 20, 2026
b828ce4
Unlock master key during vault authentication
ultraelectronica Jun 20, 2026
36264d0
Integrate EncryptionService into authentication flows
ultraelectronica Jun 20, 2026
cf9fecf
Store pending credential and increase KDF iterations
ultraelectronica Jun 20, 2026
ca51787
Implement key wrapping and biometric unlock for master keys
ultraelectronica Jun 20, 2026
da190e4
Remove deprecated compression services
ultraelectronica Jun 20, 2026
3fbee5b
Replace hardcoded SDK version with device_info_plus
ultraelectronica Jun 20, 2026
eabd51d
Update default encryption algorithm to AES-256-GCM and KDF iterations to
ultraelectronica Jun 20, 2026
91829d8
Remove unused compression helper utility
ultraelectronica Jun 20, 2026
65c8a45
Increase PBKDF2 iterations to 600000
ultraelectronica Jun 20, 2026
47cc797
Store update subscription to cancel on dispose
ultraelectronica Jun 20, 2026
6534329
Remove default Flutter widget test
ultraelectronica Jun 20, 2026
817f20a
Replace `cupertino_icons` and `encrypt` with `device_info_plus`
ultraelectronica Jun 20, 2026
509e8d9
Add CI workflow for versionF branch
ultraelectronica Jun 20, 2026
a2e2c03
Add Android data extraction and network security config files
ultraelectronica Jun 20, 2026
03ef152
Add ProGuard rules for Flutter, crypto, and autofill service
ultraelectronica Jun 20, 2026
0959482
Add isolated Argon2id hash computation using compute()
ultraelectronica Jun 20, 2026
528c43c
Add unit tests for AES-256-GCM and CTR encryption round-trips
ultraelectronica Jun 20, 2026
29dd18b
Add tests for Argon2id KWK derivation and AES-256-GCM key wrapping
ultraelectronica Jun 20, 2026
d71085f
Add PBKDF2 isolate test suite
ultraelectronica Jun 20, 2026
bd3ea0d
Refactor toast system and unify path utilities
ultraelectronica Jun 20, 2026
fadeb98
Add global navigator key
ultraelectronica Jun 20, 2026
16df3a2
Add centralized Android path resolution utility
ultraelectronica Jun 20, 2026
43a6d0e
Add constant-time string comparison utility
ultraelectronica Jun 20, 2026
e54ef21
Add unit tests for crypto hygiene and password strength checks
ultraelectronica Jun 20, 2026
393c1a5
Add password vault integration to gallery screen
ultraelectronica Jun 20, 2026
f24e4ce
Add onOpenPassword callback to FileOpenService
ultraelectronica Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: CI
on:
push:
branches: [versionF]
pull_request:
branches: [versionF]

jobs:
analyze-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- run: flutter pub get
- run: flutter analyze
- run: flutter test
20 changes: 5 additions & 15 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,10 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

analyzer:
exclude:
- "lib/examples/**"

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
avoid_print: true
4 changes: 3 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ android {
}

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.mossapps.locker"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
Expand All @@ -69,6 +68,9 @@ android {
buildTypes {
release {
signingConfig = signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Flutter
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-dontwarn io.flutter.embedding.**

# pointycastle (crypto)
-keep class org.bouncycastle.** { *; }
-dontwarn org.bouncycastle.**

# Autofill service
-keep class com.mossapps.locker.AutofillService { *; }
-keep class com.mossapps.locker.AutofillSelectionActivity { *; }
26 changes: 25 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
android:label="Latch"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"
android:requestLegacyExternalStorage="true">
android:requestLegacyExternalStorage="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:networkSecurityConfig="@xml/network_security_config">

<activity
android:name=".MainActivity"
Expand Down Expand Up @@ -78,6 +81,27 @@
android:name="flutterEmbedding"
android:value="2" />

<service
android:name=".AutofillService"
android:label="Latch"
android:exported="true"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<intent-filter>
<action android:name="android.service.autofill.AutofillService" />
</intent-filter>
<meta-data
android:name="android.autofill"
android:resource="@xml/autofill_service" />
</service>

<activity
android:name=".AutofillSelectionActivity"
android:exported="false"
android:excludeFromRecents="true"
android:theme="@style/NormalTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:windowSoftInputMode="adjustResize" />

</application>

<!-- Required to query activities that can process text, see:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.mossapps.locker

import android.app.assist.AssistStructure
import android.service.autofill.FillCallback
import android.view.autofill.AutofillId

enum class FieldType { USERNAME, PASSWORD }

data class AutofillField(val autofillId: AutofillId, val type: FieldType)

object AutofillCallbackHolder {
var callback: FillCallback? = null
private set
var fields: List<AutofillField>? = null
private set

fun set(callback: FillCallback, fields: List<AutofillField>) {
this.callback = callback
this.fields = fields
}

fun cancel() {
try { callback?.onSuccess(null) } catch (_: Exception) {}
clear()
}

fun clear() {
callback = null
fields = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.mossapps.locker

import android.app.assist.AssistStructure
import android.os.Build
import android.os.Bundle
import android.service.autofill.Dataset
import android.service.autofill.FillResponse
import android.util.Log
import android.view.View
import android.view.WindowManager
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

@RequiresApi(Build.VERSION_CODES.O)
class AutofillSelectionActivity : FlutterFragmentActivity() {

private val CHANNEL = "com.mossapps.locker/autofill"

override fun getDartEntrypointFunctionName(): String = "autofillMain"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"fillCredentials" -> {
val username = call.argument<String>("username") ?: ""
val password = call.argument<String>("password") ?: ""
val title = call.argument<String>("title") ?: "Latch"
fillCredentials(username, password, title, result)
}
"cancel" -> {
AutofillCallbackHolder.cancel()
result.success(null)
finish()
}
else -> result.notImplemented()
}
}
}

private fun fillCredentials(username: String, password: String, title: String, result: MethodChannel.Result) {
val callback = AutofillCallbackHolder.callback
val fields = AutofillCallbackHolder.fields

if (callback == null || fields == null) {
result.error("no_callback", "No fill request pending", null)
finish()
return
}

try {
val presentation = RemoteViews(packageName, R.layout.autofill_suggestion)
presentation.setTextViewText(R.id.autofill_title, title)

val builder = Dataset.Builder()
var matched = false

for (field in fields) {
when (field.type) {
FieldType.USERNAME -> {
if (username.isNotEmpty()) {
builder.setValue(field.autofillId, AutofillValue.forText(username), presentation)
matched = true
}
}
FieldType.PASSWORD -> {
if (password.isNotEmpty()) {
builder.setValue(field.autofillId, AutofillValue.forText(password), presentation)
matched = true
}
}
}
}

if (!matched) {
AutofillCallbackHolder.cancel()
result.success(null)
finish()
return
}

val response = FillResponse.Builder().addDataset(builder.build()).build()
callback.onSuccess(response)
AutofillCallbackHolder.clear()
result.success(null)
finish()
} catch (e: Exception) {
Log.e("AutofillSelection", "fill failed", e)
AutofillCallbackHolder.cancel()
result.error("fill_error", e.message, null)
finish()
}
}

override fun onDestroy() {
AutofillCallbackHolder.cancel()
super.onDestroy()
}
}
92 changes: 92 additions & 0 deletions android/app/src/main/kotlin/com/mossapps/locker/AutofillService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.mossapps.locker

import android.app.assist.AssistStructure
import android.content.Intent
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.AutofillService
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import android.service.autofill.FillResponse
import android.service.autofill.SaveRequest
import android.text.InputType
import android.view.View
import android.view.autofill.AutofillId
import androidx.annotation.RequiresApi

@RequiresApi(Build.VERSION_CODES.O)
class AutofillService : AutofillService() {

override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback) {
val structure = request.fillContexts.lastOrNull()?.structure
if (structure == null) {
callback.onSuccess(null)
return
}

val fields = parseFields(structure)
if (fields.isEmpty()) {
callback.onSuccess(null)
return
}

AutofillCallbackHolder.set(callback, fields)

val intent = Intent(this, AutofillSelectionActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
}

override fun onSaveRequest(request: SaveRequest, callback: android.service.autofill.SaveCallback) {
callback.onSuccess()
}

private fun parseFields(structure: AssistStructure): List<AutofillField> {
val fields = mutableListOf<AutofillField>()
val seenIds = mutableSetOf<AutofillId>()

for (i in 0 until structure.windowNodeCount) {
traverseNode(structure.getWindowNodeAt(i).rootViewNode, fields, seenIds)
}
return fields
}

private fun traverseNode(node: AssistStructure.ViewNode, fields: MutableList<AutofillField>, seenIds: MutableSet<AutofillId>) {
val id = node.autofillId
if (id != null && id !in seenIds) {
val type = classifyField(node)
if (type != null) {
fields.add(AutofillField(id, type))
seenIds.add(id)
}
}

for (i in 0 until node.childCount) {
traverseNode(node.getChildAt(i), fields, seenIds)
}
}

private fun classifyField(node: AssistStructure.ViewNode): FieldType? {
val hints = node.autofillHints
if (hints != null) {
for (hint in hints) {
when (hint) {
View.AUTOFILL_HINT_USERNAME, View.AUTOFILL_HINT_EMAIL_ADDRESS -> return FieldType.USERNAME
View.AUTOFILL_HINT_PASSWORD -> return FieldType.PASSWORD
}
}
}

val inputType = node.inputType
val variation = inputType and InputType.TYPE_MASK_VARIATION
when (variation) {
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
InputType.TYPE_NUMBER_VARIATION_PASSWORD -> return FieldType.PASSWORD
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> return FieldType.USERNAME
}

return null
}
}
23 changes: 23 additions & 0 deletions android/app/src/main/res/layout/autofill_suggestion.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">

<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/launcher_icon"
android:layout_marginEnd="12dp" />

<TextView
android:id="@+id/autofill_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:singleLine="true"
android:ellipsize="end" />
</LinearLayout>
3 changes: 3 additions & 0 deletions android/app/src/main/res/xml/autofill_service.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.mossapps.locker.MainActivity" />
15 changes: 15 additions & 0 deletions android/app/src/main/res/xml/data_extraction_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="root" />
</cloud-backup>
<device-transfer>
<exclude domain="sharedpref" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="root" />
</device-transfer>
</data-extraction-rules>
Loading
Loading