232 lines
8.0 KiB
Java
232 lines
8.0 KiB
Java
package org.julianfamily.keepassgo;
|
|
|
|
import android.content.Context;
|
|
import android.util.JsonReader;
|
|
import android.util.Log;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
|
|
final class AutofillCacheStore {
|
|
private static final String TAG = "KeePassGOAutofill";
|
|
|
|
private AutofillCacheStore() {
|
|
}
|
|
|
|
static Entry findBestMatch(Context context, String webDomain) {
|
|
List<Entry> entries = readEntries(context);
|
|
if (entries.isEmpty()) {
|
|
return null;
|
|
}
|
|
return fromMatcherEntry(AutofillTargetMatcher.findBestMatch(toMatcherEntries(entries), webDomain));
|
|
}
|
|
|
|
static Entry findByID(Context context, String entryID) {
|
|
if (entryID == null || entryID.trim().isEmpty()) {
|
|
return null;
|
|
}
|
|
for (Entry entry : readEntries(context)) {
|
|
if (entryID.equals(entry.id)) {
|
|
return entry;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static List<Entry> chooserCandidates(Context context, String rawTarget) {
|
|
List<Entry> entries = readEntries(context);
|
|
if (entries.isEmpty()) {
|
|
return entries;
|
|
}
|
|
Entry direct = fromMatcherEntry(AutofillTargetMatcher.findBestMatch(toMatcherEntries(entries), rawTarget));
|
|
if (direct != null) {
|
|
List<Entry> resolved = new ArrayList<>();
|
|
resolved.add(direct);
|
|
return resolved;
|
|
}
|
|
entries.sort(Comparator
|
|
.comparing((Entry entry) -> entry.title.toLowerCase(Locale.US))
|
|
.thenComparing(entry -> String.join("/", entry.path).toLowerCase(Locale.US))
|
|
.thenComparing(entry -> entry.id));
|
|
return entries;
|
|
}
|
|
|
|
private static List<AutofillTargetMatcher.Entry> toMatcherEntries(List<Entry> entries) {
|
|
List<AutofillTargetMatcher.Entry> converted = new ArrayList<>(entries.size());
|
|
for (Entry entry : entries) {
|
|
converted.add(new AutofillTargetMatcher.Entry(
|
|
entry.id,
|
|
entry.title,
|
|
entry.username,
|
|
entry.password,
|
|
entry.host,
|
|
entry.url,
|
|
entry.targets,
|
|
entry.path
|
|
));
|
|
}
|
|
return converted;
|
|
}
|
|
|
|
private static Entry fromMatcherEntry(AutofillTargetMatcher.Entry entry) {
|
|
if (entry == null) {
|
|
return null;
|
|
}
|
|
return new Entry(entry.id, entry.title, entry.username, entry.password, entry.host, entry.url, entry.targets, entry.path);
|
|
}
|
|
|
|
private static List<Entry> readEntries(Context context) {
|
|
File cacheFile = findCacheFile(context);
|
|
if (cacheFile == null) {
|
|
Log.i(TAG, "autofill cache file not found");
|
|
return new ArrayList<>();
|
|
}
|
|
try {
|
|
return readEntries(cacheFile);
|
|
} catch (IOException err) {
|
|
Log.e(TAG, "failed to read autofill cache", err);
|
|
return new ArrayList<>();
|
|
}
|
|
}
|
|
|
|
private static File findCacheFile(Context context) {
|
|
List<File> candidates = new ArrayList<>();
|
|
File filesDir = context.getFilesDir();
|
|
if (filesDir != null) {
|
|
candidates.add(new File(filesDir, "keepassgo/autofill-cache.json"));
|
|
candidates.add(new File(filesDir, ".config/keepassgo/autofill-cache.json"));
|
|
candidates.add(new File(filesDir, "config/keepassgo/autofill-cache.json"));
|
|
}
|
|
File baseDir = context.getDataDir();
|
|
if (baseDir != null) {
|
|
candidates.add(new File(baseDir, "files/keepassgo/autofill-cache.json"));
|
|
candidates.add(new File(baseDir, "files/.config/keepassgo/autofill-cache.json"));
|
|
candidates.add(new File(baseDir, "files/config/keepassgo/autofill-cache.json"));
|
|
}
|
|
for (File candidate : candidates) {
|
|
if (candidate.isFile()) {
|
|
Log.i(TAG, "using autofill cache " + candidate.getAbsolutePath());
|
|
return candidate;
|
|
}
|
|
}
|
|
Log.i(TAG, "no autofill cache file in " + candidates);
|
|
return null;
|
|
}
|
|
|
|
private static List<Entry> readEntries(File cacheFile) throws IOException {
|
|
List<Entry> entries = new ArrayList<>();
|
|
try (JsonReader reader = new JsonReader(new InputStreamReader(new FileInputStream(cacheFile), StandardCharsets.UTF_8))) {
|
|
reader.beginObject();
|
|
while (reader.hasNext()) {
|
|
String name = reader.nextName();
|
|
if ("entries".equals(name)) {
|
|
reader.beginArray();
|
|
while (reader.hasNext()) {
|
|
entries.add(readEntry(reader));
|
|
}
|
|
reader.endArray();
|
|
} else {
|
|
reader.skipValue();
|
|
}
|
|
}
|
|
reader.endObject();
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
private static Entry readEntry(JsonReader reader) throws IOException {
|
|
String id = "";
|
|
String title = "";
|
|
String username = "";
|
|
String password = "";
|
|
String host = "";
|
|
String url = "";
|
|
List<String> targets = new ArrayList<>();
|
|
List<String> path = new ArrayList<>();
|
|
reader.beginObject();
|
|
while (reader.hasNext()) {
|
|
String name = reader.nextName();
|
|
switch (name) {
|
|
case "id":
|
|
id = nextString(reader);
|
|
break;
|
|
case "title":
|
|
title = nextString(reader);
|
|
break;
|
|
case "username":
|
|
username = nextString(reader);
|
|
break;
|
|
case "password":
|
|
password = nextString(reader);
|
|
break;
|
|
case "url":
|
|
url = nextString(reader);
|
|
break;
|
|
case "host":
|
|
host = normalizeHost(nextString(reader));
|
|
break;
|
|
case "targets":
|
|
reader.beginArray();
|
|
while (reader.hasNext()) {
|
|
targets.add(nextString(reader));
|
|
}
|
|
reader.endArray();
|
|
break;
|
|
case "path":
|
|
reader.beginArray();
|
|
while (reader.hasNext()) {
|
|
path.add(nextString(reader));
|
|
}
|
|
reader.endArray();
|
|
break;
|
|
default:
|
|
reader.skipValue();
|
|
break;
|
|
}
|
|
}
|
|
reader.endObject();
|
|
return new Entry(id, title, username, password, host, url, targets, path);
|
|
}
|
|
|
|
private static String nextString(JsonReader reader) throws IOException {
|
|
if (reader.peek() == android.util.JsonToken.NULL) {
|
|
reader.nextNull();
|
|
return "";
|
|
}
|
|
return reader.nextString();
|
|
}
|
|
|
|
private static String normalizeHost(String raw) {
|
|
return AutofillTargetMatcher.normalize(raw).host;
|
|
}
|
|
|
|
static final class Entry {
|
|
final String id;
|
|
final String title;
|
|
final String username;
|
|
final String password;
|
|
final String host;
|
|
final String url;
|
|
final List<String> targets;
|
|
final List<String> path;
|
|
|
|
Entry(String id, String title, String username, String password, String host, String url, List<String> targets, List<String> path) {
|
|
this.id = id;
|
|
this.title = title;
|
|
this.username = username;
|
|
this.password = password;
|
|
this.host = host;
|
|
this.url = url;
|
|
this.targets = new ArrayList<>(targets);
|
|
this.path = new ArrayList<>(path);
|
|
}
|
|
}
|
|
}
|