Re-add keepass2android plugin
This commit is contained in:
@@ -29,6 +29,9 @@
|
|||||||
|
|
||||||
## ⭐ Features
|
## ⭐ 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
|
- Search entries in Keepass Database
|
||||||
- Scan & Create entries
|
- Scan & Create entries
|
||||||
- Recently used history for fast access
|
- Recently used history for fast access
|
||||||
|
|||||||
@@ -1,20 +1,45 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-feature android:name="android.hardware.camera" />
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||||
|
|
||||||
|
<!-- Keepass2Android plugin mode: make the KP2A activities resolvable from this app -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="keepass2android.ACTION_START_WITH_TASK" />
|
||||||
|
</intent>
|
||||||
|
<package android:name="keepass2android.keepass2android" />
|
||||||
|
<package android:name="keepass2android.keepass2android_nonet" />
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:icon="@mipmap/ic_launcher_round"
|
android:icon="@mipmap/ic_launcher_round"
|
||||||
android:label="${APP_NAME}"
|
android:label="${APP_NAME}"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.MainActivity"
|
android:name=".activity.MainActivity"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".pluginSDK.PluginAccessBroadcastReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
tools:ignore="ExportedReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" />
|
||||||
|
<action android:name="keepass2android.ACTION_RECEIVE_ACCESS" />
|
||||||
|
<action android:name="keepass2android.ACTION_REVOKE_ACCESS" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -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?>): String? {
|
||||||
|
if (values.isEmpty()) return null
|
||||||
|
val a = JSONArray()
|
||||||
|
values.forEach { a.put(it) }
|
||||||
|
return a.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stringToStringArray(s: String?): ArrayList<String> {
|
||||||
|
val strings = ArrayList<String>()
|
||||||
|
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<String?>
|
||||||
|
) {
|
||||||
|
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?>): 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<String?>,
|
||||||
|
availableScopes: ArrayList<String>
|
||||||
|
): 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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<String, String>,
|
||||||
|
protectedFields: ArrayList<String>?
|
||||||
|
): 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<String, String> {
|
||||||
|
val res = HashMap<String, String>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String?> = ArrayList(
|
||||||
|
listOf(
|
||||||
|
Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
<p><i>Keepass-Fidelity</i> adds an interface to view&save barcodes (QR included) with a Keepass Database.</p><p><br></p><ul><li><b>Launcher:</b> view and launch recent entries (a per entry flag can disable this behaviour)</li><li><b>View:</b> view entries from loaded from the database</li><li><b>Create:</b> add entries from the camera, an image of by filling out a form. The entry is then created in the database</li><li><b>Data:</b> 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).</li></ul>
|
<p><i>Keepass-Fidelity</i> 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.</p><p><br></p><ul><li><b>Launcher:</b> view and launch recent entries (a per entry flag can disable this behaviour)</li><li><b>View:</b> view entries from loaded from the database</li><li><b>Create:</b> add entries from the camera, an image of by filling out a form. The entry is then created in the database</li><li><b>Data:</b> 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).</li></ul>
|
||||||
Reference in New Issue
Block a user