diff --git a/README.md b/README.md index ad4b1c4..341d0d2 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ ## ⭐ Features +- Two storage modes, chosen on first start (switchable later from the launcher): + - **Standalone**: opens a `.kdbx` database directly (bundled KeePassDX engine) + - **Keepass2Android plugin**: queries and creates entries through the installed Keepass2Android app - Search entries in Keepass Database - Scan & Create entries - Recently used history for fast access diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d4ea7a..27f952c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,20 +1,45 @@ - + + + + + + + + + + + + + + android:exported="true" + android:launchMode="singleTask"> + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt new file mode 100644 index 0000000..5dcc395 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt @@ -0,0 +1,89 @@ +package net.helcel.fidelity.pluginSDK + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import org.json.JSONArray +import org.json.JSONException + + +object AccessManager { + private const val PREF_KEY_SCOPE = "scope" + private const val PREF_KEY_TOKEN = "token" + private const val PREF_HOSTS = "KP2A.PluginAccess.hosts" + + private fun stringArrayToString(values: ArrayList): String? { + if (values.isEmpty()) return null + val a = JSONArray() + values.forEach { a.put(it) } + return a.toString() + } + + private fun stringToStringArray(s: String?): ArrayList { + val strings = ArrayList() + if (s.isNullOrEmpty()) return strings + + try { + val a = JSONArray(s) + for (i in 0 until a.length()) + strings.add(a.optString(i)) + } catch (e: JSONException) { + e.printStackTrace() + } + return strings + } + + fun storeAccessToken( + ctx: Context, + hostPackage: String?, + accessToken: String?, + scopes: ArrayList + ) { + getPrefsForHost(ctx, hostPackage).edit { + putString(PREF_KEY_TOKEN, accessToken) + putString(PREF_KEY_SCOPE, stringArrayToString(scopes)) + } + + val hostPrefs = ctx.getSharedPreferences(PREF_HOSTS, Context.MODE_PRIVATE) + if (!hostPrefs.contains(hostPackage)) + hostPrefs.edit { putString(hostPackage, "") } + } + + private fun getPrefsForHost( + ctx: Context, + hostPackage: String? + ): SharedPreferences { + return ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE) + } + + fun tryGetAccessToken(ctx: Context, hostPackage: String?, scopes: ArrayList): String? { + if (hostPackage.isNullOrEmpty()) return null + + val prefs = getPrefsForHost(ctx, hostPackage) + val scopesString = prefs.getString(PREF_KEY_SCOPE, "") + val currentScope = stringToStringArray(scopesString) + if (!isSubset(scopes, currentScope)) + return null + return prefs.getString(PREF_KEY_TOKEN, null) + } + + private fun isSubset( + requiredScopes: ArrayList, + availableScopes: ArrayList + ): Boolean { + return availableScopes.containsAll(requiredScopes) + } + + fun removeAccessToken( + ctx: Context, hostPackage: String?, + accessToken: String? + ) { + val prefs = getPrefsForHost(ctx, hostPackage) + if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) + prefs.edit { clear() } + + val hostPrefs = ctx.getSharedPreferences(PREF_HOSTS, Context.MODE_PRIVATE) + if (hostPrefs.contains(hostPackage)) + hostPrefs.edit { remove(hostPackage) } + } +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/KeepassDef.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/KeepassDef.kt new file mode 100644 index 0000000..9b61168 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/KeepassDef.kt @@ -0,0 +1,9 @@ +package net.helcel.fidelity.pluginSDK + +@Suppress("unused") +object KeepassDef { + var TitleField: String = "Title" + var UserNameField: String = "UserName" + var PasswordField: String = "Password" + var UrlField: String = "URL" +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt new file mode 100644 index 0000000..22dd49b --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt @@ -0,0 +1,49 @@ +package net.helcel.fidelity.pluginSDK + +import android.content.Intent +import org.json.JSONException +import org.json.JSONObject + +object Kp2aControl { + + fun getAddEntryIntent( + fields: HashMap, + protectedFields: ArrayList? + ): Intent { + val outputData = JSONObject((fields as Map<*, *>)).toString() + val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK) + startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT) + startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask") + startKp2aIntent.putExtra("ShowUserNotifications", "true") + startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData) + if (protectedFields != null) + startKp2aIntent.putStringArrayListExtra( + Strings.EXTRA_PROTECTED_FIELDS_LIST, + protectedFields + ) + return startKp2aIntent + } + + fun getQueryEntryForOwnPackageIntent(): Intent { + return Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE) + } + + fun getEntryFieldsFromIntent(intent: Intent?): HashMap { + val res = HashMap() + try { + val json = JSONObject(intent?.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA) ?: "") + val itr = json.keys() + while (itr.hasNext()) { + val key = itr.next() + val value = json[key].toString() + res[key] = value + } + } catch (e: JSONException) { + e.printStackTrace() + } catch (e: NullPointerException) { + e.printStackTrace() + } + return res + } +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessBroadcastReceiver.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessBroadcastReceiver.kt new file mode 100644 index 0000000..8280f7a --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessBroadcastReceiver.kt @@ -0,0 +1,51 @@ +package net.helcel.fidelity.pluginSDK + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class PluginAccessBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + val action = intent.action ?: return + when (action) { + Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent) + Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent) + Strings.ACTION_REVOKE_ACCESS -> revokeAccess(ctx, intent) + else -> {} + } + } + + private fun revokeAccess(ctx: Context, intent: Intent) { + val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) + val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) + AccessManager.removeAccessToken(ctx, senderPackage, accessToken) + } + + + private fun receiveAccess(ctx: Context, intent: Intent) { + val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) + val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) + AccessManager.storeAccessToken(ctx, senderPackage, accessToken, scopes) + } + + private fun requestAccess(ctx: Context, intent: Intent) { + val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) + val requestToken = intent.getStringExtra(Strings.EXTRA_REQUEST_TOKEN) + val rpi = Intent(Strings.ACTION_REQUEST_ACCESS) + rpi.setPackage(senderPackage) + rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName) + rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken) + + val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage, scopes) + rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token) + + rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes) + ctx.sendBroadcast(rpi) + } + + private val scopes: ArrayList = ArrayList( + listOf( + Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE, + ) + ) +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt new file mode 100644 index 0000000..02df096 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt @@ -0,0 +1,32 @@ +package net.helcel.fidelity.pluginSDK + +@Suppress("unused") +object Strings { + + const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS" + const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY" + const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = + "keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" + const val SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS" + + const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES" + const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE" + + const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER" + const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN" + const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK" + + const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" + const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS" + const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS" + const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS" + + + const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA" + const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST" + const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN" + const val EXTRA_ENTRY_ID = "keepass2android.EXTRA_ENTRY_ID" + + const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = + "keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" +} diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt index 8945ac3..4c2a112 100644 --- a/metadata/en-US/full_description.txt +++ b/metadata/en-US/full_description.txt @@ -1 +1 @@ -

Keepass-Fidelity adds an interface to view&save barcodes (QR included) with a Keepass Database.


  • Launcher: view and launch recent entries (a per entry flag can disable this behaviour)
  • View: view entries from loaded from the database
  • Create: add entries from the camera, an image of by filling out a form. The entry is then created in the database
  • Data: the app uses the following data Title (entry name), barcode type (QR, UPC, ...), barcode content (number/text content) and a "secure" flag (enable/disable caching the entry).
\ No newline at end of file +

Keepass-Fidelity adds an interface to view&save barcodes (QR included) with a Keepass Database, either by opening a KDBX file directly (standalone) or through the Keepass2Android plugin interface.


  • Launcher: view and launch recent entries (a per entry flag can disable this behaviour)
  • View: view entries from loaded from the database
  • Create: add entries from the camera, an image of by filling out a form. The entry is then created in the database
  • Data: the app uses the following data Title (entry name), barcode type (QR, UPC, ...), barcode content (number/text content) and a "secure" flag (enable/disable caching the entry).
\ No newline at end of file