From bd99b928d981c1872e1f3af7d771a8c3931f7c0f Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Wed, 1 Apr 2026 10:36:08 -0700 Subject: [PATCH] Honor alternate autofill targets from entry fields --- android/keepassgo-android.jar | Bin 14014 -> 16101 bytes .../keepassgo/AutofillCacheStore.java | 104 ++++++++++---- autofillcache/cache.go | 136 ++++++++++++++---- autofillcache/cache_test.go | 59 ++++++++ 4 files changed, 250 insertions(+), 49 deletions(-) diff --git a/android/keepassgo-android.jar b/android/keepassgo-android.jar index 1c3579ff6d78522b3d8227bb08f0c65cec4f7d09..89509e5c6aea7fbffb3d10992116b5d3d13bdf95 100644 GIT binary patch delta 10842 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 diff --git a/androidsrc/org/julianfamily/keepassgo/AutofillCacheStore.java b/androidsrc/org/julianfamily/keepassgo/AutofillCacheStore.java index b48cd4d..770f949 100644 --- a/androidsrc/org/julianfamily/keepassgo/AutofillCacheStore.java +++ b/androidsrc/org/julianfamily/keepassgo/AutofillCacheStore.java @@ -42,11 +42,11 @@ final class AutofillCacheStore { List exactHost = new ArrayList<>(); List parentHost = new ArrayList<>(); for (Entry entry : entries) { - if (entry.host.equals(target.host)) { + if (entryMatchesHost(entry, target.host)) { exactHost.add(entry); continue; } - if (!entry.host.isEmpty() && target.host.endsWith("." + entry.host)) { + if (entryMatchesParentHost(entry, target.host)) { parentHost.add(entry); } } @@ -108,6 +108,7 @@ final class AutofillCacheStore { String password = ""; String host = ""; String url = ""; + List targets = new ArrayList<>(); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); @@ -127,13 +128,20 @@ final class AutofillCacheStore { case "host": host = normalizeHost(nextString(reader)); break; + case "targets": + reader.beginArray(); + while (reader.hasNext()) { + targets.add(nextString(reader)); + } + reader.endArray(); + break; default: reader.skipValue(); break; } } reader.endObject(); - return new Entry(title, username, password, host, url); + return new Entry(title, username, password, host, url, targets); } private static String nextString(JsonReader reader) throws IOException { @@ -209,16 +217,21 @@ final class AutofillCacheStore { List exact = new ArrayList<>(); List prefix = new ArrayList<>(); + int bestPrefixLen = -1; for (Entry entry : entries) { - NormalizedTarget entryTarget = normalizeURL(entry.url); - if (entryTarget.host.isEmpty()) { - continue; - } - if (entryTarget.url.equals(target.url)) { + MatchQuality quality = bestTargetMatch(entry, target); + if (quality.exact) { exact.add(entry); continue; } - if (!"/".equals(entryTarget.path) && target.path.startsWith(entryTarget.path)) { + if (quality.prefixLength <= 0) { + continue; + } + if (quality.prefixLength > bestPrefixLen) { + prefix.clear(); + prefix.add(entry); + bestPrefixLen = quality.prefixLength; + } else if (quality.prefixLength == bestPrefixLen) { prefix.add(entry); } } @@ -229,23 +242,54 @@ final class AutofillCacheStore { return null; } - Entry best = null; - int bestLen = -1; - boolean ambiguous = false; - for (Entry entry : prefix) { - int pathLen = normalizeURL(entry.url).path.length(); - if (pathLen > bestLen) { - best = entry; - bestLen = pathLen; - ambiguous = false; - } else if (pathLen == bestLen) { - ambiguous = true; + return prefix.size() == 1 ? prefix.get(0) : null; + } + + private static boolean entryMatchesHost(Entry entry, String host) { + for (NormalizedTarget target : entryTargets(entry)) { + if (target.host.equals(host)) { + return true; } } - if (ambiguous) { - return null; + return false; + } + + private static boolean entryMatchesParentHost(Entry entry, String host) { + for (NormalizedTarget target : entryTargets(entry)) { + if (!target.host.isEmpty() && host.endsWith("." + target.host)) { + return true; + } } - return best; + return false; + } + + private static List entryTargets(Entry entry) { + List rawTargets = entry.targets; + if (rawTargets.isEmpty()) { + rawTargets = new ArrayList<>(); + rawTargets.add(entry.url); + } + List targets = new ArrayList<>(); + for (String rawTarget : rawTargets) { + NormalizedTarget target = normalizeURL(rawTarget); + if (!target.host.isEmpty()) { + targets.add(target); + } + } + return targets; + } + + private static MatchQuality bestTargetMatch(Entry entry, NormalizedTarget target) { + int bestPrefixLen = -1; + for (NormalizedTarget entryTarget : entryTargets(entry)) { + if (entryTarget.url.equals(target.url)) { + return new MatchQuality(true, 0); + } + if (!"/".equals(entryTarget.path) && target.path.startsWith(entryTarget.path)) { + bestPrefixLen = Math.max(bestPrefixLen, entryTarget.path.length()); + } + } + return new MatchQuality(false, bestPrefixLen); } static final class Entry { @@ -254,13 +298,25 @@ final class AutofillCacheStore { final String password; final String host; final String url; + final List targets; - Entry(String title, String username, String password, String host, String url) { + Entry(String title, String username, String password, String host, String url, List targets) { this.title = title; this.username = username; this.password = password; this.host = host; this.url = url; + this.targets = new ArrayList<>(targets); + } + } + + private static final class MatchQuality { + final boolean exact; + final int prefixLength; + + MatchQuality(boolean exact, int prefixLength) { + this.exact = exact; + this.prefixLength = prefixLength; } } diff --git a/autofillcache/cache.go b/autofillcache/cache.go index efd7837..a3f2531 100644 --- a/autofillcache/cache.go +++ b/autofillcache/cache.go @@ -19,6 +19,7 @@ type Entry struct { Password string `json:"password"` URL string `json:"url"` Host string `json:"host"` + Targets []string `json:"targets,omitempty"` Path []string `json:"path,omitempty"` } @@ -36,11 +37,11 @@ func Match(cache File, webURL string) (Entry, bool) { exactHost := make([]Entry, 0) parentHost := make([]Entry, 0) for _, entry := range cache.Entries { - if entry.Host == target.host { + if entryMatchesHost(entry, target.host) { exactHost = append(exactHost, entry) continue } - if entry.Host != "" && strings.HasSuffix(target.host, "."+entry.Host) { + if entryMatchesParentHost(entry, target.host) { parentHost = append(parentHost, entry) } } @@ -54,7 +55,16 @@ func Match(cache File, webURL string) (Entry, bool) { func Build(model vault.Model, now time.Time) File { entries := make([]Entry, 0, len(model.Entries)) for _, item := range model.Entries { + targets := collectTargets(item) host := normalizeHost(item.URL) + if host == "" { + for _, target := range targets { + host = normalizeHost(target) + if host != "" { + break + } + } + } if host == "" { continue } @@ -68,6 +78,7 @@ func Build(model vault.Model, now time.Time) File { Password: item.Password, URL: item.URL, Host: host, + Targets: targets, Path: append([]string(nil), item.Path...), }) } @@ -150,18 +161,23 @@ func chooseEntry(target normalizedTarget, entries []Entry) (Entry, bool) { } exact := make([]Entry, 0) - prefix := make([]Entry, 0) + bestPrefixLen := -1 + bestPrefix := make([]Entry, 0) for _, entry := range entries { - entryTarget := normalizeURL(entry.URL) - if entryTarget.host == "" { - continue - } - if entryTarget.url == target.url { + exactMatch, prefixLen := bestTargetMatch(entry, target) + if exactMatch { exact = append(exact, entry) continue } - if entryTarget.path != "/" && strings.HasPrefix(target.path, entryTarget.path) { - prefix = append(prefix, entry) + if prefixLen <= 0 { + continue + } + switch { + case prefixLen > bestPrefixLen: + bestPrefixLen = prefixLen + bestPrefix = []Entry{entry} + case prefixLen == bestPrefixLen: + bestPrefix = append(bestPrefix, entry) } } if len(exact) == 1 { @@ -170,22 +186,92 @@ func chooseEntry(target normalizedTarget, entries []Entry) (Entry, bool) { if len(exact) > 1 { return Entry{}, false } - if len(prefix) == 0 { + if len(bestPrefix) == 1 { + return bestPrefix[0], true + } + if len(bestPrefix) == 0 { return Entry{}, false } - - sort.Slice(prefix, func(i, j int) bool { - return len(normalizeURL(prefix[i].URL).path) > len(normalizeURL(prefix[j].URL).path) - }) - bestPath := normalizeURL(prefix[0].URL).path - best := make([]Entry, 0, len(prefix)) - for _, entry := range prefix { - if normalizeURL(entry.URL).path == bestPath { - best = append(best, entry) - } - } - if len(best) == 1 { - return best[0], true - } return Entry{}, false } + +func collectTargets(item vault.Entry) []string { + seen := make(map[string]struct{}) + targets := make([]string, 0, 1+len(item.Fields)) + appendTarget := func(raw string) { + value := strings.TrimSpace(raw) + if value == "" { + return + } + if _, ok := seen[value]; ok { + return + } + seen[value] = struct{}{} + targets = append(targets, value) + } + + appendTarget(item.URL) + + keys := make([]string, 0, len(item.Fields)) + for key := range item.Fields { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + upper := strings.ToUpper(strings.TrimSpace(key)) + if strings.HasPrefix(upper, "ANDROIDAPP") || strings.HasPrefix(upper, "KP2A_URL") { + appendTarget(item.Fields[key]) + } + } + + return targets +} + +func entryTargets(entry Entry) []normalizedTarget { + values := entry.Targets + if len(values) == 0 { + values = []string{entry.URL} + } + targets := make([]normalizedTarget, 0, len(values)) + for _, value := range values { + target := normalizeURL(value) + if target.host == "" { + continue + } + targets = append(targets, target) + } + return targets +} + +func entryMatchesHost(entry Entry, host string) bool { + for _, target := range entryTargets(entry) { + if target.host == host { + return true + } + } + return false +} + +func entryMatchesParentHost(entry Entry, host string) bool { + for _, target := range entryTargets(entry) { + if target.host != "" && strings.HasSuffix(host, "."+target.host) { + return true + } + } + return false +} + +func bestTargetMatch(entry Entry, target normalizedTarget) (bool, int) { + bestPrefixLen := -1 + for _, candidate := range entryTargets(entry) { + if candidate.url == target.url { + return true, 0 + } + if candidate.path != "/" && strings.HasPrefix(target.path, candidate.path) { + if pathLen := len(candidate.path); pathLen > bestPrefixLen { + bestPrefixLen = pathLen + } + } + } + return false, bestPrefixLen +} diff --git a/autofillcache/cache_test.go b/autofillcache/cache_test.go index face179..f9762d5 100644 --- a/autofillcache/cache_test.go +++ b/autofillcache/cache_test.go @@ -36,6 +36,10 @@ func TestBuildFiltersAndNormalizesEntries(t *testing.T) { Username: "user", Password: "pass", URL: "surveillance.crew.example.invalid", + Fields: map[string]string{ + "AndroidApp1": "androidapp://com.lights.mobile", + "KP2A_URL_1": "https://surveillance.crew.example.invalid/account", + }, }, }, }, now) @@ -49,6 +53,9 @@ func TestBuildFiltersAndNormalizesEntries(t *testing.T) { if got.Entries[1].Host != "surveillance.crew.example.invalid" { t.Fatalf("second host = %q, want surveillance.crew.example.invalid", got.Entries[1].Host) } + if len(got.Entries[1].Targets) != 3 { + t.Fatalf("len(second targets) = %d, want 3", len(got.Entries[1].Targets)) + } if got.UpdatedAt != "2026-03-31T12:00:00Z" { t.Fatalf("updatedAt = %q", got.UpdatedAt) } @@ -235,3 +242,55 @@ func TestMatchRejectsAmbiguousAndroidAppPackageTargets(t *testing.T) { t.Fatalf("Match() unexpectedly resolved ambiguous android app package target") } } + +func TestMatchUsesAndroidAppCustomFieldTarget(t *testing.T) { + t.Parallel() + + cache := File{ + Entries: []Entry{ + { + ID: "one", + Title: "Blink", + Username: "blink-user", + Password: "secret1", + URL: "https://account.blinknetwork.com", + Host: "account.blinknetwork.com", + Targets: []string{"https://account.blinknetwork.com", "androidapp://com.blinknetwork.mobile2"}, + }, + }, + } + + got, ok := Match(cache, "androidapp://com.blinknetwork.mobile2") + if !ok { + t.Fatalf("Match() found no entry") + } + if got.ID != "one" { + t.Fatalf("Match() entry = %q, want one", got.ID) + } +} + +func TestMatchUsesKP2AURLCustomFieldTarget(t *testing.T) { + t.Parallel() + + cache := File{ + Entries: []Entry{ + { + ID: "one", + Title: "Blink", + Username: "blink-user", + Password: "secret1", + URL: "https://blinknetwork.com", + Host: "blinknetwork.com", + Targets: []string{"https://blinknetwork.com", "https://account.blinknetwork.com"}, + }, + }, + } + + got, ok := Match(cache, "https://account.blinknetwork.com") + if !ok { + t.Fatalf("Match() found no entry") + } + if got.ID != "one" { + t.Fatalf("Match() entry = %q, want one", got.ID) + } +}