rename back to current names

This commit is contained in:
Philipp Crocoll
2025-02-11 13:53:55 +01:00
parent 8ebe1bb0d9
commit 38aaa91c5b
784 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
using System;
using Android.Content.PM;
using Android.Content.Res;
using Android.Graphics.Drawables;
using Android.Widget;
using Android.Content;
using Android.Views;
using System.Collections.Generic;
using Android.App;
using Android.Runtime;
using keepass2android;
namespace keepass2android
{
/// <summary>
/// Represents information about a plugin for display in the plugin list activity
/// </summary>
public class PluginItem
{
private readonly string _package;
private readonly Context _ctx;
private readonly Resources _pluginRes;
public PluginItem(string package, string enabledStatus, Context ctx)
{
_package = package;
_ctx = ctx;
EnabledStatus = enabledStatus;
_pluginRes = _ctx.PackageManager.GetResourcesForApplication(_package);
}
public string Label
{
get
{
return PluginDetailsActivity.GetStringFromPlugin(_pluginRes, _package, "kp2aplugin_title");
}
}
public string Version
{
get
{
return _ctx.PackageManager.GetPackageInfo(_package, 0).VersionName;
}
}
public string EnabledStatus
{
get;
set;
}
public Drawable Icon
{
get
{
return _ctx.PackageManager.GetApplicationIcon(_package);
}
}
public string Package
{
get { return _package; }
}
}
public class PluginArrayAdapter : ArrayAdapter<PluginItem>
{
class PluginViewHolder : Java.Lang.Object
{
public ImageView imgIcon;
public TextView txtTitle;
public TextView txtVersion;
public TextView txtEnabledStatus;
}
Context context;
int layoutResourceId;
IList<PluginItem> data = null;
public PluginArrayAdapter(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public PluginArrayAdapter(Context context, int layoutResourceId, IList<PluginItem> data) :
base(context, layoutResourceId, data)
{
this.layoutResourceId = layoutResourceId;
this.context = context;
this.data = data;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
View row = convertView;
PluginViewHolder holder = null;
if (row == null)
{
LayoutInflater inflater = ((Activity)context).LayoutInflater;
row = inflater.Inflate(layoutResourceId, parent, false);
holder = new PluginViewHolder();
holder.imgIcon = (ImageView)row.FindViewById(Resource.Id.imgIcon);
holder.txtTitle = (TextView)row.FindViewById(Resource.Id.txtLabel);
holder.txtVersion = (TextView)row.FindViewById(Resource.Id.txtVersion);
holder.txtEnabledStatus = (TextView)row.FindViewById(Resource.Id.txtStatus);
row.Tag = holder;
}
else
{
holder = (PluginViewHolder)row.Tag;
}
var item = data[position];
holder.txtTitle.Text = item.Label;
holder.txtVersion.Text = item.Version;
holder.txtEnabledStatus.Text = item.EnabledStatus;
holder.imgIcon.SetImageDrawable(item.Icon);
return row;
}
}
}

View File

@@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Util;
using Keepass2android.Pluginsdk;
namespace keepass2android
{
public class PluginDatabase
{
public class KeyGenerator
{
public static string GetUniqueKey(int maxSize)
{
char[] chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
byte[] data = new byte[1];
RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider();
crypto.GetNonZeroBytes(data);
data = new byte[maxSize];
crypto.GetNonZeroBytes(data);
StringBuilder result = new StringBuilder(maxSize);
foreach (byte b in data)
{
result.Append(chars[b % (chars.Length)]);
}
return result.ToString();
}
}
private const string _tag = "KP2A_PluginDatabase";
private readonly Context _ctx;
private const string _accessToken = "accessToken";
private const string _scopes = "scopes";
private const string _requesttoken = "requestToken";
public PluginDatabase(Context ctx)
{
_ctx = ctx;
}
private ISharedPreferences GetPreferencesForPlugin(string packageName)
{
var prefs = _ctx.GetSharedPreferences("KP2A.Plugin." + packageName, FileCreationMode.Private);
if (prefs.GetString(_requesttoken, null) == null)
{
var editor = prefs.Edit();
editor.PutString(_requesttoken, Guid.NewGuid().ToString());
editor.Commit();
}
return prefs;
}
/// <summary>
/// Returns the request token for the plugin. Request token is created of not yet available.
/// </summary>
/// <returns>Request token. Never null or empty.</returns>
public string GetRequestToken(string pkgName)
{
return GetPreferencesForPlugin(pkgName).GetString(_requesttoken, null);
}
public IList<string> GetPluginScopes(string pluginPackage)
{
var prefs = GetPreferencesForPlugin(pluginPackage);
return AccessManager.StringToStringArray(prefs.GetString(_scopes, ""));
}
public IEnumerable<String> GetAllPluginPackages()
{
return PluginHost.GetAllPlugins(_ctx);
}
public bool IsPackageInstalled(string targetPackage)
{
try
{
PackageInfo info = _ctx.PackageManager.GetPackageInfo(targetPackage, PackageInfoFlags.MetaData);
}
catch (PackageManager.NameNotFoundException e)
{
return false;
}
return true;
}
public bool IsEnabled(string pluginPackage)
{
return GetPreferencesForPlugin(pluginPackage).GetString(_accessToken, null) != null;
}
public void StorePlugin(string pluginPackage, string accessToken, IList<string> requestedScopes)
{
ISharedPreferences pluginPrefs = GetPreferencesForPlugin(pluginPackage);
pluginPrefs.Edit()
.PutString(_scopes, AccessManager.StringArrayToString(requestedScopes))
.PutString(_accessToken, accessToken)
.Commit();
}
public void SetEnabled(string pluginPackage, bool enabled)
{
if (enabled)
{
string accessToken = KeyGenerator.GetUniqueKey(32);
Intent i = new Intent(Strings.ActionReceiveAccess);
i.SetPackage(pluginPackage);
i.PutExtra(Strings.ExtraSender, _ctx.PackageName);
i.PutExtra(Strings.ExtraRequestToken, GetPreferencesForPlugin(pluginPackage).GetString(_requesttoken, null));
i.PutExtra(Strings.ExtraAccessToken, accessToken);
_ctx.SendBroadcast(i);
StorePlugin(pluginPackage, accessToken, GetPluginScopes(pluginPackage));
}
else
{
Intent i = new Intent(Strings.ActionRevokeAccess);
i.SetPackage(pluginPackage);
i.PutExtra(Strings.ExtraSender, _ctx.PackageName);
i.PutExtra(Strings.ExtraRequestToken, GetPreferencesForPlugin(pluginPackage).GetString(_requesttoken, null));
_ctx.SendBroadcast(i);
StorePlugin(pluginPackage, null, GetPluginScopes(pluginPackage));
}
}
public bool IsValidAccessToken(string pluginPackage, string accessToken, string scope)
{
if (pluginPackage == null)
{
Log.Warn(_tag, "No pluginPackage specified!");
return false;
}
if (accessToken == null)
{
Log.Warn(_tag, "No accessToken specified!");
return false;
}
//internal access token is valid for all scopes
if ((pluginPackage == _ctx.PackageName) && (accessToken == GetInternalToken()))
return true;
var prefs = GetPreferencesForPlugin(pluginPackage);
if (prefs.GetString(_accessToken, null) != accessToken)
{
Log.Warn(_tag, "Invalid access token for " + pluginPackage);
return false;
}
if (!AccessManager.StringToStringArray(prefs.GetString(_scopes, "")).Contains(scope))
{
Log.Warn(_tag, "Scope " + scope + " not granted for " + pluginPackage);
return false;
}
return true;
}
public string GetAccessToken(string pluginPackage)
{
return GetPreferencesForPlugin(pluginPackage).GetString(_accessToken, null);
}
public void Clear()
{
foreach (string plugin in GetAllPluginPackages())
{
GetPreferencesForPlugin(plugin).Edit().Clear().Commit();
}
}
public IEnumerable<string> GetPluginsWithAcceptedScope(string scope)
{
return GetAllPluginPackages().Where(plugin =>
{
var prefs = GetPreferencesForPlugin(plugin);
return (prefs.GetString(_accessToken, null) != null)
&& AccessManager.StringToStringArray(prefs.GetString(_scopes, "")).Contains(scope);
});
}
public void ClearPlugin(string plugin)
{
var prefs = _ctx.GetSharedPreferences("KP2A.Plugin." + plugin, FileCreationMode.Private);
prefs.Edit().Clear().Commit();
}
/// <summary>
/// Checks if the given pluginPackage has been granted the requiredScope
/// </summary>
public bool HasAcceptedScope(string pluginPackage, string requiredScope)
{
if (pluginPackage == null)
{
Log.Warn(_tag, "No pluginPackage specified!");
return false;
}
var prefs = GetPreferencesForPlugin(pluginPackage);
if (prefs.GetString(_accessToken, null) == null)
{
Log.Info(_tag, "No access token for " + pluginPackage);
return false;
}
if (!AccessManager.StringToStringArray(prefs.GetString(_scopes, "")).Contains(requiredScope))
{
Log.Info(_tag, "Scope " + requiredScope + " not granted for " + pluginPackage);
return false;
}
return true;
}
public string GetInternalToken()
{
var prefs = _ctx.GetSharedPreferences("KP2A.PluginHost" , FileCreationMode.Private);
if (prefs.Contains(_accessToken))
{
return prefs.GetString(_accessToken, null);
}
else
{
var token = KeyGenerator.GetUniqueKey(32);
prefs.Edit().PutString(_accessToken, token).Commit();
return token;
}
}
}
}

View File

@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Content.Res;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Util;
using Android.Views;
using Android.Widget;
using Java.Util;
using Keepass2android.Pluginsdk;
using keepass2android.views;
using keepass2android;
namespace keepass2android
{
[Activity(Label = AppNames.AppName, Theme = "@style/android:Theme.Material.Light", Exported = true)]
[IntentFilter(new[] { Strings.ActionEditPluginSettings },
Label = AppNames.AppName,
Categories = new[] { Intent.CategoryDefault })]
public class PluginDetailsActivity : Activity
{
private CheckBox _checkbox;
private string _pluginPackageName;
protected override void OnCreate(Bundle bundle)
{
new ActivityDesign(this).ApplyTheme();
base.OnCreate(bundle);
_pluginPackageName = Intent.GetStringExtra(Strings.ExtraPluginPackage);
var pluginRes = PackageManager.GetResourcesForApplication(_pluginPackageName);
var title = GetStringFromPlugin(pluginRes, _pluginPackageName, "kp2aplugin_title");
var author = GetStringFromPlugin(pluginRes, _pluginPackageName, "kp2aplugin_author");
var shortDesc = GetStringFromPlugin(pluginRes, _pluginPackageName, "kp2aplugin_shortdesc");
var version = PackageManager.GetPackageInfo(_pluginPackageName, 0).VersionName;
SetContentView(Resource.Layout.plugin_details);
if (title != null)
FindViewById<TextView>(Resource.Id.txtLabel).Text = title;
FindViewById<TextView>(Resource.Id.txtVersion).Text = version;
SetTextOrHide(Resource.Id.txtAuthor, author);
SetTextOrHide(Resource.Id.txtShortDesc, shortDesc);
_checkbox = FindViewById<CheckBox>(Resource.Id.cb_enabled);
_checkbox.CheckedChange += delegate
{
new PluginDatabase(this).SetEnabled(_pluginPackageName, _checkbox.Checked);
};
Drawable d = PackageManager.GetApplicationIcon(_pluginPackageName);
FindViewById<ImageView>(Resource.Id.imgIcon).SetImageDrawable(d);
FindViewById<TextView>(Resource.Id.txtVersion).Text = version;
//cannot be wrong to update the view when we received an update
PluginHost.OnReceivedRequest += OnPluginHostOnOnReceivedRequest;
if (Intent.Action == Strings.ActionEditPluginSettings)
{
//this action can be triggered by external apps so we don't know if anybody has ever triggered
//the plugin to request access. Do this now, don't set the view right now
PluginHost.TriggerRequest(this, _pluginPackageName, new PluginDatabase(this));
//show the buttons instead of the checkbox
_checkbox.Visibility = ViewStates.Invisible;
FindViewById(Resource.Id.accept_button).Visibility = ViewStates.Invisible; //show them only after access is requested
FindViewById(Resource.Id.deny_button).Visibility = ViewStates.Visible;
FindViewById(Resource.Id.accept_button).Click += delegate(object sender, EventArgs args)
{
new PluginDatabase(this).SetEnabled(_pluginPackageName, true);
SetResult(Result.Ok);
Finish();
};
FindViewById(Resource.Id.deny_button).Click += delegate(object sender, EventArgs args)
{
new PluginDatabase(this).SetEnabled(_pluginPackageName, false);
SetResult(Result.Canceled);
Finish();
};
//in case the plugin requested scopes previously, make sure we display them
UpdateView();
}
else
{
UpdateView();
_checkbox.Visibility = ViewStates.Visible;
FindViewById(Resource.Id.accept_button).Visibility = ViewStates.Gone;
FindViewById(Resource.Id.deny_button).Visibility = ViewStates.Gone;
}
}
private void OnPluginHostOnOnReceivedRequest(object sender, PluginHost.PluginHostEventArgs args)
{
if (args.Package == _pluginPackageName)
{
FindViewById(Resource.Id.accept_button).Visibility = ViewStates.Visible;
UpdateView();
}
}
protected override void OnDestroy()
{
PluginHost.OnReceivedRequest -= OnPluginHostOnOnReceivedRequest;
base.OnDestroy();
}
private void UpdateView()
{
var scopesContainer = FindViewById<LinearLayout>(Resource.Id.scopes_list);
scopesContainer.RemoveAllViews();
var pluginDb = new PluginDatabase(this);
_checkbox.Checked = pluginDb.IsEnabled(_pluginPackageName);
foreach (string scope in pluginDb.GetPluginScopes(_pluginPackageName))
{
string scopeId = scope.Substring("keepass2android.".Length);
TextWithHelp help = new TextWithHelp(this,
GetString(Resources.GetIdentifier(scopeId + "_title", "string", PackageName)),
GetString(Resources.GetIdentifier(scopeId + "_explanation", "string", PackageName)));
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FillParent,
ViewGroup.LayoutParams.WrapContent);
help.LayoutParameters = layoutParams;
scopesContainer.AddView(help);
}
}
private void SetTextOrHide(int resourceId, string text)
{
if (text != null)
{
FindViewById<TextView>(resourceId).Text = text;
FindViewById<TextView>(resourceId).Visibility = ViewStates.Visible;
}
else
FindViewById<TextView>(resourceId).Visibility = ViewStates.Gone;
}
public static string GetStringFromPlugin(Resources pluginRes, string pluginPackage, string stringId)
{
int titleId = pluginRes.GetIdentifier(pluginPackage + ":string/" + stringId, null, null);
string title = null;
if (titleId != 0)
title = pluginRes.GetString(titleId);
return title;
}
}
}

View File

@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Util;
using Group.Pals.Android.Lib.UI.Filechooser.Utils;
using KeePassLib.Utility;
using Keepass2android.Pluginsdk;
using Org.Json;
namespace keepass2android
{
/// <summary>
/// Class which manages plugins inside the app
/// </summary>
[BroadcastReceiver(Exported = true)]
[IntentFilter(new[] { Strings.ActionRequestAccess })]
public class PluginHost : BroadcastReceiver
{
private const string _tag = "KP2A_PluginHost";
private static readonly string[] _validScopes = { Strings.ScopeDatabaseActions,
Strings.ScopeCurrentEntry,
Strings.ScopeQueryCredentials,
Strings.ScopeQueryCredentialsForOwnPackage};
public static IEnumerable<string> GetAllPlugins(Context ctx)
{
Intent accessIntent = new Intent(Strings.ActionTriggerRequestAccess);
PackageManager packageManager = ctx.PackageManager;
IList<ResolveInfo> dictPacks = packageManager.QueryBroadcastReceivers(
accessIntent, PackageInfoFlags.Receivers);
return dictPacks.Select(ri => ri.ActivityInfo.ApplicationInfo).Select(appInfo => appInfo.PackageName);
}
/// <summary>
/// Sends a broadcast to all potential plugins prompting them to request access to our app.
/// </summary>
public static void TriggerRequests(Context ctx)
{
PluginDatabase pluginDatabase = new PluginDatabase(ctx);
foreach (string pkg in GetAllPlugins(ctx))
TriggerRequest(ctx, pkg, pluginDatabase);
}
public static void TriggerRequest(Context ctx, string pkgName, PluginDatabase pluginDatabase)
{
try
{
Intent triggerIntent = new Intent(Strings.ActionTriggerRequestAccess);
triggerIntent.SetPackage(pkgName);
triggerIntent.PutExtra(Strings.ExtraSender, ctx.PackageName);
string requestToken = pluginDatabase.GetRequestToken(pkgName);
triggerIntent.PutExtra(Strings.ExtraRequestToken, requestToken);
Android.Util.Log.Debug(_tag, "Request token: " + requestToken);
ctx.SendBroadcast(triggerIntent);
}
catch (Exception e)
{
}
}
public override void OnReceive(Context context, Intent intent)
{
PluginDatabase pluginDb = new PluginDatabase(context);
if (intent.Action == Strings.ActionRequestAccess)
{
string senderPackage = intent.GetStringExtra(Strings.ExtraSender);
string requestToken = intent.GetStringExtra(Strings.ExtraRequestToken);
IList<string> requestedScopes = intent.GetStringArrayListExtra(Strings.ExtraScopes);
if (!AreScopesValid(requestedScopes))
{
Log.Debug(_tag, "requested scopes not valid");
return;
}
if (pluginDb.GetRequestToken(senderPackage) != requestToken)
{
Log.Warn(_tag, "Invalid requestToken!");
return;
}
string currentAccessToken = pluginDb.GetAccessToken(senderPackage);
if ((currentAccessToken != null)
&& (AccessManager.IsSubset(requestedScopes,
pluginDb.GetPluginScopes(senderPackage))))
{
//permission already there.
var i = new Intent(Strings.ActionReceiveAccess);
i.PutExtra(Strings.ExtraSender, context.PackageName);
i.PutExtra(Strings.ExtraAccessToken, currentAccessToken);
//TODO: Plugin should verify requestToken to make sure it doesn't receive accessTokens from malicious apps
i.PutExtra(Strings.ExtraRequestToken, requestToken);
i.SetPackage(senderPackage);
context.SendBroadcast(i);
Log.Debug(_tag, "Plugin " + senderPackage + " enabled.");
}
else
{
//store that scope was requested but not yet approved (=> accessToken = null)
pluginDb.StorePlugin(senderPackage, null, requestedScopes);
Log.Debug(_tag, "Plugin " + senderPackage + " not enabled.");
//see if the plugin has an access token
string accessToken = intent.GetStringExtra(Strings.ExtraAccessToken);
if (accessToken != null)
{
//notify plugin that access token is no longer valid or sufficient
Intent i = new Intent(Strings.ActionRevokeAccess);
i.PutExtra(Strings.ExtraSender, context.PackageName);
i.PutExtra(Strings.ExtraAccessToken, accessToken);
i.SetPackage(senderPackage);
context.SendBroadcast(i);
Log.Warn(_tag, "Access token of plugin " + senderPackage + " not (or no more) valid.");
}
}
if (OnReceivedRequest != null)
OnReceivedRequest(this, new PluginHostEventArgs() { Package = senderPackage });
}
}
private bool AreScopesValid(IList<string> requestedScopes)
{
foreach (string scope in requestedScopes)
{
if (!_validScopes.Contains(scope))
{
Log.Warn(_tag, "invalid scope: " + scope);
return false;
}
}
return true;
}
/// <summary>
/// adds the entry output data to the intent to be sent to a plugin
/// </summary>
public static void AddEntryToIntent(Intent intent, PwEntryOutput entry)
{
/*//add the entry XML
not yet implemented. What to do with attachments?
MemoryStream memStream = new MemoryStream();
KdbxFile.WriteEntries(memStream, new[] {entry});
string entryData = StrUtil.Utf8.GetString(memStream.ToArray());
intent.PutExtra(Strings.ExtraEntryData, entryData);
*/
//add the output string array (placeholders replaced taking into account the db context)
Dictionary<string, string> outputFields = entry.OutputStrings.ToDictionary(pair => StrUtil.SafeXmlString(pair.Key), pair => pair.Value.ReadString());
JSONObject jsonOutput = new JSONObject(outputFields);
var jsonOutputStr = jsonOutput.ToString();
intent.PutExtra(Strings.ExtraEntryOutputData, jsonOutputStr);
JSONArray jsonProtectedFields = new JSONArray(
(System.Collections.ICollection)entry.OutputStrings
.Where(pair => pair.Value.IsProtected)
.Select(pair => pair.Key)
.ToArray());
intent.PutExtra(Strings.ExtraProtectedFieldsList, jsonProtectedFields.ToString());
intent.PutExtra(Strings.ExtraEntryId, entry.Uuid.ToHexString());
}
public class PluginHostEventArgs
{
public string Package { get; set; }
}
public static event EventHandler<PluginHostEventArgs> OnReceivedRequest;
}
}

View File

@@ -0,0 +1,72 @@
using System.Collections.Generic;
using System.Linq;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Widget;
using Keepass2android.Pluginsdk;
using keepass2android;
namespace keepass2android
{
[Activity(Label = "@string/plugins", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden, Theme="@style/android:Theme.Material.Light", Exported = true)]
[IntentFilter(new[] { "kp2a.action.PluginListActivity" }, Categories = new[] { Intent.CategoryDefault })]
public class PluginListActivity : ListActivity
{
private PluginArrayAdapter _pluginArrayAdapter;
private List<PluginItem> _items;
protected override void OnCreate(Bundle bundle)
{
new ActivityDesign(this).ApplyTheme();
base.OnCreate(bundle);
SetContentView(Resource.Layout.plugin_list);
PluginHost.TriggerRequests(this);
ListView listView = FindViewById<ListView>(Android.Resource.Id.List);
listView.ItemClick +=
(sender, args) =>
{
Intent i = new Intent(this, typeof(PluginDetailsActivity));
i.PutExtra(Strings.ExtraPluginPackage, _items[args.Position].Package);
StartActivity(i);
};
FindViewById<Button>(Resource.Id.btnPluginsOnline).Click += delegate
{
Util.GotoUrl(this, "https://github.com/PhilippC/keepass2android/blob/master/docs/Available-Plug-ins.md");
};
}
protected override void OnResume()
{
PluginDatabase pluginDb = new PluginDatabase(this);
_items = (from pluginPackage in pluginDb.GetAllPluginPackages()
let version = PackageManager.GetPackageInfo(pluginPackage, 0).VersionName
let enabledStatus = pluginDb.IsEnabled(pluginPackage) ? GetString(Resource.String.plugin_enabled) : GetString(Resource.String.plugin_disabled)
select new PluginItem(pluginPackage, enabledStatus, this)).ToList();
/*
{
new PluginItem("PluginA", Resource.Drawable.Icon, "keepass2android.plugina", "connected"),
new PluginItem("KeepassNFC", Resource.Drawable.Icon, "com.bla.blubb.plugina", "disconnected")
};
* */
_pluginArrayAdapter = new PluginArrayAdapter(this, Resource.Layout.ListViewPluginRow, _items);
ListAdapter = _pluginArrayAdapter;
base.OnResume();
}
protected override void OnPause()
{
base.OnPause();
ListAdapter = _pluginArrayAdapter = null;
}
}
}