Skip to content

Commit 62bf70e

Browse files
committed
Initial implementation of handling CredentialProviderService:OnBeginCreateCredentialRequest
1 parent d4b70bc commit 62bf70e

File tree

8 files changed

+283
-1
lines changed

8 files changed

+283
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,4 @@ src/java/Keepass2AndroidPluginSDK2/build/generated/mockable-Google-Inc.-Google-A
178178
/src/MegaTest
179179
*.dtbcache.json
180180
/src/keepass2android-app/AndroidManifest.xml
181+
*.DS_Store

.vscode/launch.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "C#: keepass2android-app Debug",
9+
"type": "dotnet",
10+
"request": "launch",
11+
"projectPath": "${workspaceFolder}/src/keepass2android-app/keepass2android-app.csproj"
12+
}
13+
]
14+
}

src/Kp2aBusinessLogic/database/edit/CreateDB.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace keepass2android
2828

2929
public class CreateDb : OperationWithFinishHandler
3030
{
31+
public const string DefaultDbName = "Keepass2Android Password Database";
3132
private readonly IOConnectionInfo _ioc;
3233
private readonly bool _dontSave;
3334
private readonly IKp2aApp _app;
@@ -68,7 +69,7 @@ public override void Run()
6869
db.KpDatabase.New(_ioc, _key, _app.GetFileStorage(_ioc).GetFilenameWithoutPathAndExt(_ioc));
6970

7071
db.KpDatabase.KdfParameters = (new AesKdf()).GetDefaultParameters();
71-
db.KpDatabase.Name = "Keepass2Android Password Database";
72+
db.KpDatabase.Name = DefaultDbName;
7273
//re-set the name of the root group because the PwDatabase uses UrlUtil which is not appropriate for all file storages:
7374
db.KpDatabase.RootGroup.Name = _app.GetFileStorage(_ioc).GetFilenameWithoutPathAndExt(_ioc);
7475

src/keepass2android-app/Resources/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,5 +1350,8 @@
13501350

13511351
<string name="pref_periodic_background_sync_interval_title">Periodic background synchronization time interval</string>
13521352
<string name="pref_periodic_background_sync_interval_summary">Set the interval for background synchronization in minutes.</string>
1353+
1354+
<string name="credential_provider_service_subtitle">Password credential provider</string>
1355+
<string name="credential_provider_password_creation_description">Save Password in new entry</string>
13531356

13541357
</resources>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
4+
5+
This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on
6+
Keepassdroid, Copyright Brian Pellin.
7+
8+
Keepass2Android is free software: you can redistribute it and/or modify
9+
it under the terms of the GNU General Public License as published by
10+
the Free Software Foundation, either version 2 of the License, or
11+
(at your option) any later version.
12+
13+
Keepass2Android is distributed in the hope that it will be useful,
14+
but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
GNU General Public License for more details.
17+
18+
You should have received a copy of the GNU General Public License
19+
along with Keepass2Android. If not, see <http://www.gnu.org/licenses/>.
20+
-->
21+
<!--TODO
22+
set android:settingsActivity to AppSettingsActivity-->
23+
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android"
24+
xmlns:tools="http://schemas.android.com/tools"
25+
android:settingsSubtitle="@string/credential_provider_service_subtitle"
26+
tools:targetApi="34">
27+
<capabilities>
28+
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
29+
</capabilities>
30+
</credential-provider>

src/keepass2android-app/keepass2android-app.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,7 @@
745745
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.30" />
746746
<PackageReference Include="Xamarin.AndroidX.Biometric" Version="1.1.0.27" />
747747
<PackageReference Include="Xamarin.AndroidX.CoordinatorLayout" Version="1.3.0" />
748+
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.5.0" />
748749
<PackageReference Include="Xamarin.AndroidX.CursorAdapter" Version="1.0.0.31" />
749750
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Common" Version="2.8.7.2" />
750751
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.8.7.2" />
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using Android.Content;
2+
using Android.Content.PM;
3+
using Android.Runtime;
4+
using Android.Views;
5+
using AndroidX.Credentials;
6+
using AndroidX.Credentials.Exceptions;
7+
using AndroidX.Credentials.Provider;
8+
using Keepass2android.Pluginsdk;
9+
using KeePassLib;
10+
using Org.Json;
11+
12+
namespace keepass2android.services.Kp2aCredentialProvider
13+
{
14+
[Activity(Label = AppNames.AppName,
15+
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden,
16+
Theme = "@style/Kp2aTheme_ActionBar",
17+
WindowSoftInputMode = SoftInput.AdjustResize,
18+
Permission = "keepass2android." + AppNames.PackagePart + ".permission.Kp2aChooseAutofill")]
19+
public class Kp2aCredentialLauncherActivity : AndroidX.AppCompat.App.AppCompatActivity
20+
{
21+
public const string CredentialRequestTypeKey = "credential_request_type";
22+
public const int CredentialRequestTypeCreatePassword = 1;
23+
private const int CreateEntryRequestCode = 100;
24+
25+
protected override void OnCreate(Bundle? savedInstanceState)
26+
{
27+
base.OnCreate(savedInstanceState);
28+
29+
var intent = Intent;
30+
if (intent == null)
31+
{
32+
// should never happen
33+
SetResult(Result.Canceled);
34+
Finish();
35+
}
36+
else
37+
{
38+
var requestType = intent.GetIntExtra(CredentialRequestTypeKey, 0);
39+
switch (requestType)
40+
{
41+
case CredentialRequestTypeCreatePassword:
42+
HandleCreatePasswordRequest(intent);
43+
break;
44+
45+
default:
46+
// should never happen
47+
SetResult(Result.Canceled);
48+
Finish();
49+
break;
50+
}
51+
}
52+
}
53+
54+
private void HandleCreatePasswordRequest(Intent intent)
55+
{
56+
var createRequest = PendingIntentHandler.RetrieveProviderCreateCredentialRequest(intent);
57+
if (createRequest != null && createRequest?.CallingRequest is CreatePasswordRequest)
58+
{
59+
if (createRequest.CallingRequest is not CreatePasswordRequest request)
60+
{
61+
SetUpFailureResponseForCreateAndFinish("Unable to extract request from intent");
62+
}
63+
else
64+
{
65+
var callingPackage = createRequest.CallingAppInfo.PackageName;
66+
67+
var forwardIntent = new Intent(this, typeof(SelectCurrentDbActivity));
68+
69+
Dictionary<string, string> outputFields = [];
70+
if (callingPackage != null)
71+
{
72+
outputFields.TryAdd(PwDefs.UrlField, $"{KeePass.AndroidAppScheme}{callingPackage}");
73+
}
74+
75+
outputFields.TryAdd(PwDefs.UserNameField, request.Id);
76+
outputFields.TryAdd(PwDefs.PasswordField, request.Password);
77+
78+
JSONObject jsonOutput = new(outputFields);
79+
var jsonOutputStr = jsonOutput.ToString();
80+
forwardIntent.PutExtra(Strings.ExtraEntryOutputData, jsonOutputStr);
81+
82+
JSONArray jsonProtectedFields = new(
83+
(System.Collections.ICollection)Array.Empty<string>());
84+
forwardIntent.PutExtra(Strings.ExtraProtectedFieldsList, jsonProtectedFields.ToString());
85+
86+
forwardIntent.PutExtra(AppTask.AppTaskKey, "CreateEntryThenCloseTask");
87+
forwardIntent.PutExtra(CreateEntryThenCloseTask.ShowUserNotificationsKey, "false");
88+
StartActivityForResult(forwardIntent, CreateEntryRequestCode);
89+
}
90+
}
91+
else
92+
{
93+
SetUpFailureResponseForCreateAndFinish("Unable to extract request from intent");
94+
}
95+
}
96+
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent? data)
97+
{
98+
base.OnActivityResult(requestCode, resultCode, data);
99+
if (requestCode == CreateEntryRequestCode)
100+
{
101+
var result = new Intent();
102+
if (resultCode == KeePass.ExitCloseAfterTaskComplete)
103+
{
104+
PendingIntentHandler.SetCreateCredentialResponse(
105+
result,
106+
new CreatePasswordResponse()
107+
);
108+
SetResult(Result.Ok, result);
109+
}
110+
else
111+
{
112+
PendingIntentHandler.SetCreateCredentialException(
113+
result,
114+
new CreateCredentialCancellationException()
115+
);
116+
SetResult(Result.Canceled, result);
117+
}
118+
if (!IsFinishing)
119+
{
120+
Finish();
121+
}
122+
}
123+
}
124+
125+
/// <summary>
126+
/// If the request is null, send an unknown exception to client and finish the flow.
127+
/// </summary>
128+
/// <param name="message">The error message to send to the client.</param>
129+
private void SetUpFailureResponseForCreateAndFinish(string message)
130+
{
131+
var result = new Intent();
132+
PendingIntentHandler.SetCreateCredentialException(
133+
result,
134+
new CreateCredentialUnknownException(message)
135+
);
136+
SetResult(Result.Ok, result);
137+
Finish();
138+
}
139+
}
140+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
2+
//
3+
// Keepass2Android is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// Keepass2Android is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with Keepass2Android. If not, see <http://www.gnu.org/licenses/>.
15+
16+
using System.Runtime.Versioning;
17+
using Android.Content;
18+
using Android.Graphics;
19+
using Android.Graphics.Drawables;
20+
using Android.OS;
21+
using AndroidX.Credentials.Exceptions;
22+
using AndroidX.Credentials.Provider;
23+
using Java.Interop;
24+
using Java.Util.Concurrent.Atomic;
25+
26+
namespace keepass2android.services.Kp2aCredentialProvider
27+
{
28+
[Service(Enabled = true, Exported = true, Permission = "android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE")]
29+
[IntentFilter(actions: ["android.service.credentials.CredentialProviderService"])]
30+
[MetaData(name: "android.credentials.provider", Resource = "@xml/credentials_provider")]
31+
[SupportedOSPlatform("android31.0")]
32+
public class Kp2aCredentialProviderService : CredentialProviderService
33+
{
34+
private readonly AtomicInteger requestCode = new();
35+
public override void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request, CancellationSignal cancellationSignal, IOutcomeReceiver callback)
36+
{
37+
Kp2aLog.Log("Kp2aCredentialProviderService:onBeginCreateCredentialRequest called");
38+
if (request is BeginCreatePasswordCredentialRequest)
39+
{
40+
var currentDb = App.Kp2a.CurrentDb;
41+
var accountName = currentDb?.KpDatabase?.Name ?? CreateDb.DefaultDbName;
42+
var blendMode = BlendMode.Dst;
43+
var icon = blendMode == null
44+
? null
45+
: Icon.CreateWithResource(this, AppNames.LauncherIcon)?.SetTintBlendMode(blendMode);
46+
47+
callback.OnResult(
48+
new BeginCreateCredentialResponse.Builder()
49+
.AddCreateEntry(
50+
new CreateEntry
51+
.Builder(
52+
accountName,
53+
PendingIntent.GetActivity(
54+
ApplicationContext,
55+
requestCode.IncrementAndGet(),
56+
new Intent(this, typeof(Kp2aCredentialLauncherActivity))
57+
.PutExtra(
58+
Kp2aCredentialLauncherActivity.CredentialRequestTypeKey,
59+
Kp2aCredentialLauncherActivity.CredentialRequestTypeCreatePassword
60+
),
61+
PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent
62+
)!
63+
)
64+
.SetIcon(icon)
65+
.SetDescription(
66+
GetString(Resource.String.credential_provider_password_creation_description)
67+
)
68+
// Set the last used time to "now"
69+
// so the active account is the default option in the system prompt.
70+
.SetLastUsedTime(currentDb == null ? null : Java.Time.Instant.Now())
71+
.Build()
72+
).Build()
73+
);
74+
}
75+
else
76+
{
77+
callback.OnError(new CreateCredentialUnsupportedException()
78+
.JavaCast<Java.Lang.Object>());
79+
}
80+
}
81+
82+
public override void OnBeginGetCredentialRequest(BeginGetCredentialRequest request, CancellationSignal cancellationSignal, IOutcomeReceiver callback)
83+
{
84+
//TODO implement Kp2aCredentialProviderService:OnBeginGetCredentialRequest
85+
}
86+
87+
public override void OnClearCredentialStateRequest(ProviderClearCredentialStateRequest request, CancellationSignal cancellationSignal, IOutcomeReceiver callback)
88+
{
89+
//TODO implement Kp2aCredentialProviderService:OnClearCredentialStateRequest
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)