keepass2android/src/PluginHostTest/PluginHost.cs

181 lines
5.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Util;
using KeePassLib.Utility;
using Keepass2android.Pluginsdk;
using Org.Json;
namespace keepass2android
{
/// <summary>
/// Class which manages plugins inside the app
/// </summary>
[BroadcastReceiver]
[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);
triggerIntent.PutExtra(Strings.ExtraRequestToken, pluginDatabase.GetRequestToken(pkgName));
ctx.SendBroadcast(triggerIntent);
}
catch (Exception e)
{
}
}
public override void OnReceive(Context context, Intent intent)
{
PluginDatabase pluginDb = new PluginDatabase(context);
if (intent.Action == Strings.ActionRequestAccess)
{
var senderPackage = intent.GetStringExtra(Strings.ExtraSender);
var requestToken = intent.GetStringExtra(Strings.ExtraRequestToken);
var requestedScopes = intent.GetStringArrayListExtra(Strings.ExtraScopes);
if (!AreScopesValid(requestedScopes))
{
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;
}
}