1
0
mirror of https://github.com/bitwarden/mobile synced 2025-01-30 10:15:10 +01:00

[BEEEP] Support for automatic TOTP token copy via external autofill (Android) (#2220)

* Android: Support for automatic TOTP copy via external autofill

* update iOS autofill interface

* additional tweaks
This commit is contained in:
mp-bw 2022-12-05 12:49:34 -05:00 committed by GitHub
parent bafd9ff85d
commit 6973a0b71c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 115 additions and 43 deletions

View File

@ -15,7 +15,7 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest> <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix> <MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix> <MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<TargetFrameworkVersion>v12.1</TargetFrameworkVersion> <TargetFrameworkVersion>v13.0</TargetFrameworkVersion>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType> <AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<NuGetPackageImportStamp> <NuGetPackageImportStamp>
</NuGetPackageImportStamp> </NuGetPackageImportStamp>
@ -77,12 +77,12 @@
<PackageReference Include="Portable.BouncyCastle"> <PackageReference Include="Portable.BouncyCastle">
<Version>1.9.0</Version> <Version>1.9.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1" /> <PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1.1" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.13" /> <PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.14" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.16" /> <PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.17" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0" /> <PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0.1" />
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.14" /> <PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.15" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1" /> <PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1.1" />
<PackageReference Include="Xamarin.Essentials"> <PackageReference Include="Xamarin.Essentials">
<Version>1.7.3</Version> <Version>1.7.3</Version>
</PackageReference> </PackageReference>
@ -103,8 +103,10 @@
<Compile Include="Accessibility\Browser.cs" /> <Compile Include="Accessibility\Browser.cs" />
<Compile Include="Accessibility\NodeList.cs" /> <Compile Include="Accessibility\NodeList.cs" />
<Compile Include="Accessibility\KnownUsernameField.cs" /> <Compile Include="Accessibility\KnownUsernameField.cs" />
<Compile Include="Autofill\AutofillConstants.cs" />
<Compile Include="Autofill\AutofillHelpers.cs" /> <Compile Include="Autofill\AutofillHelpers.cs" />
<Compile Include="Autofill\AutofillService.cs" /> <Compile Include="Autofill\AutofillService.cs" />
<Compile Include="Autofill\AutofillExternalSelectionActivity.cs" />
<Compile Include="Autofill\Field.cs" /> <Compile Include="Autofill\Field.cs" />
<Compile Include="Autofill\FieldCollection.cs" /> <Compile Include="Autofill\FieldCollection.cs" />
<Compile Include="Autofill\FilledItem.cs" /> <Compile Include="Autofill\FilledItem.cs" />

View File

@ -0,0 +1,10 @@
namespace Bit.Droid.Autofill
{
public class AutofillConstants
{
public const string AutofillFramework = "autofillFramework";
public const string AutofillFrameworkFillType = "autofillFrameworkFillType";
public const string AutofillFrameworkUri = "autofillFrameworkUri";
public const string AutofillFrameworkCipherId = "autofillFrameworkCipherId";
}
}

View File

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using Android.App;
using Android.Content.PM;
using Android.OS;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Bit.Droid.Utilities;
namespace Bit.Droid.Autofill
{
[Activity(
NoHistory = true,
LaunchMode = LaunchMode.SingleTop)]
public class AutofillExternalSelectionActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle bundle)
{
Intent?.Validate();
base.OnCreate(bundle);
var cipherId = Intent?.GetStringExtra(AutofillConstants.AutofillFrameworkCipherId);
if (string.IsNullOrEmpty(cipherId))
{
SetResult(Result.Canceled);
Finish();
return;
}
GetCipherAndPerformAutofillAsync(cipherId).FireAndForget();
}
private async Task GetCipherAndPerformAutofillAsync(string cipherId)
{
var cipherService = ServiceContainer.Resolve<ICipherService>();
var cipher = await cipherService.GetAsync(cipherId);
var decCipher = await cipher.DecryptAsync();
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
autofillHandler.Autofill(decCipher);
}
}
}

View File

@ -207,7 +207,7 @@ namespace Bit.Droid.Autofill
} }
} }
var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, items[i], var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, items[i],
inlinePresentationSpec); true, inlinePresentationSpec);
if (dataset != null) if (dataset != null)
{ {
responseBuilder.AddDataset(dataset); responseBuilder.AddDataset(dataset);
@ -221,7 +221,7 @@ namespace Bit.Droid.Autofill
} }
public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem, public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem,
InlinePresentationSpec inlinePresentationSpec = null) bool includeAuthIntent, InlinePresentationSpec inlinePresentationSpec = null)
{ {
var overlayPresentation = BuildOverlayPresentation( var overlayPresentation = BuildOverlayPresentation(
filledItem.Name, filledItem.Name,
@ -242,6 +242,15 @@ namespace Bit.Droid.Autofill
{ {
datasetBuilder.SetInlinePresentation(inlinePresentation); datasetBuilder.SetInlinePresentation(inlinePresentation);
} }
if (includeAuthIntent)
{
var intent = new Intent(context, typeof(AutofillExternalSelectionActivity));
intent.PutExtra(AutofillConstants.AutofillFramework, true);
intent.PutExtra(AutofillConstants.AutofillFrameworkCipherId, filledItem.Id);
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);
}
if (filledItem.ApplyToFields(fields, datasetBuilder)) if (filledItem.ApplyToFields(fields, datasetBuilder))
{ {
return datasetBuilder.Build(); return datasetBuilder.Build();
@ -253,25 +262,26 @@ namespace Bit.Droid.Autofill
IList<InlinePresentationSpec> inlinePresentationSpecs = null) IList<InlinePresentationSpec> inlinePresentationSpecs = null)
{ {
var intent = new Intent(context, typeof(MainActivity)); var intent = new Intent(context, typeof(MainActivity));
intent.PutExtra("autofillFramework", true); intent.PutExtra(AutofillConstants.AutofillFramework, true);
if (fields.FillableForLogin) if (fields.FillableForLogin)
{ {
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Login); intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Login);
} }
else if (fields.FillableForCard) else if (fields.FillableForCard)
{ {
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Card); intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Card);
} }
else if (fields.FillableForIdentity) else if (fields.FillableForIdentity)
{ {
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Identity); intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Identity);
} }
else else
{ {
return null; return null;
} }
intent.PutExtra("autofillFrameworkUri", uri); intent.PutExtra(AutofillConstants.AutofillFrameworkUri, uri);
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true)); var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
var overlayPresentation = BuildOverlayPresentation( var overlayPresentation = BuildOverlayPresentation(
AppResources.AutofillWithBitwarden, AppResources.AutofillWithBitwarden,

View File

@ -23,6 +23,7 @@ namespace Bit.Droid.Autofill
public FilledItem(CipherView cipher) public FilledItem(CipherView cipher)
{ {
Id = cipher.Id;
Name = cipher.Name; Name = cipher.Name;
Type = cipher.Type; Type = cipher.Type;
Subtitle = cipher.SubTitle; Subtitle = cipher.SubTitle;
@ -55,6 +56,7 @@ namespace Bit.Droid.Autofill
} }
} }
public string Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Subtitle { get; set; } = string.Empty; public string Subtitle { get; set; } = string.Empty;
public int Icon { get; set; } = Resource.Drawable.login; public int Icon { get; set; } = Resource.Drawable.login;

View File

@ -18,6 +18,7 @@ using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Droid.Autofill;
using Bit.Droid.Receivers; using Bit.Droid.Receivers;
using Bit.Droid.Utilities; using Bit.Droid.Utilities;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -322,13 +323,13 @@ namespace Bit.Droid
{ {
var options = new AppOptions var options = new AppOptions
{ {
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"), Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri),
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false), MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false), GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false), FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
CreateSend = GetCreateSendRequest(Intent) CreateSend = GetCreateSendRequest(Intent)
}; };
var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0); var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);
if (fillType > 0) if (fillType > 0)
{ {
options.FillType = (CipherType)fillType; options.FillType = (CipherType)fillType;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.11.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.11.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@ -40,10 +40,4 @@
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
<!-- Package visibility (for Android 11+) -->
<queries>
<intent>
<action android:name="*" />
</intent>
</queries>
</manifest> </manifest>

View File

@ -1,4 +1,5 @@
using Android.Content; using Android.Content;
using Android.OS;
namespace Bit.Droid.Receivers namespace Bit.Droid.Receivers
{ {
@ -8,7 +9,17 @@ namespace Bit.Droid.Receivers
public override void OnReceive(Context context, Intent intent) public override void OnReceive(Context context, Intent intent)
{ {
var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager; var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager;
if (clipboardManager == null)
{
return;
}
// ClearPrimaryClip is supported down to API 28 with mixed results, so we're requiring 33+ instead
if ((int)Build.VERSION.SdkInt < 33)
{
clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " "); clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " ");
return;
}
clipboardManager.ClearPrimaryClip();
} }
} }
} }

View File

@ -73,12 +73,12 @@ namespace Bit.Droid.Services
public void Autofill(CipherView cipher) public void Autofill(CipherView cipher)
{ {
var activity = (MainActivity)CrossCurrentActivity.Current.Activity; var activity = CrossCurrentActivity.Current.Activity as Xamarin.Forms.Platform.Android.FormsAppCompatActivity;
if (activity == null) if (activity == null)
{ {
return; return;
} }
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false) if (activity.Intent?.GetBooleanExtra(AutofillConstants.AutofillFramework, false) ?? false)
{ {
if (cipher == null) if (cipher == null)
{ {
@ -103,7 +103,7 @@ namespace Bit.Droid.Services
return; return;
} }
var task = CopyTotpAsync(cipher); var task = CopyTotpAsync(cipher);
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher)); var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher), false);
var replyIntent = new Intent(); var replyIntent = new Intent();
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset); replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
activity.SetResult(Result.Ok, replyIntent); activity.SetResult(Result.Ok, replyIntent);

View File

@ -6,7 +6,6 @@ using Android.OS;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Droid.Receivers; using Bit.Droid.Receivers;
using Bit.Droid.Utilities; using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
using Xamarin.Essentials; using Xamarin.Essentials;
namespace Bit.Droid.Services namespace Bit.Droid.Services
@ -21,9 +20,9 @@ namespace Bit.Droid.Services
_stateService = stateService; _stateService = stateService;
_clearClipboardPendingIntent = new Lazy<PendingIntent>(() => _clearClipboardPendingIntent = new Lazy<PendingIntent>(() =>
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity, PendingIntent.GetBroadcast(Application.Context,
0, 0,
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)), new Intent(Application.Context, typeof(ClearClipboardAlarmReceiver)),
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false))); AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)));
} }
@ -45,7 +44,7 @@ namespace Bit.Droid.Services
} }
catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to")) catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to"))
{ {
// #1962 Just ignore, the content is copied either way but there is some app interfiering in the process // #1962 Just ignore, the content is copied either way but there is some app interfering in the process
// that the OS catches and just throws this exception. // that the OS catches and just throws this exception.
} }
} }
@ -58,9 +57,7 @@ namespace Bit.Droid.Services
private void CopyToClipboard(string text, bool isSensitive = true) private void CopyToClipboard(string text, bool isSensitive = true)
{ {
var activity = (MainActivity)CrossCurrentActivity.Current.Activity; var clipboardManager = Application.Context.GetSystemService(Context.ClipboardService) as ClipboardManager;
var clipboardManager = activity.GetSystemService(
Context.ClipboardService) as Android.Content.ClipboardManager;
var clipData = ClipData.NewPlainText("bitwarden", text); var clipData = ClipData.NewPlainText("bitwarden", text);
if (isSensitive) if (isSensitive)
{ {
@ -87,7 +84,7 @@ namespace Bit.Droid.Services
return; return;
} }
var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + clearMs; var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + clearMs;
var alarmManager = CrossCurrentActivity.Current.Activity.GetSystemService(Context.AlarmService) as AlarmManager; var alarmManager = Application.Context.GetSystemService(Context.AlarmService) as AlarmManager;
alarmManager.Set(AlarmType.Rtc, triggerMs, _clearClipboardPendingIntent.Value); alarmManager.Set(AlarmType.Rtc, triggerMs, _clearClipboardPendingIntent.Value);
} }
} }

View File

@ -69,14 +69,17 @@ namespace Bit.Droid.Services
public bool LaunchApp(string appName) public bool LaunchApp(string appName)
{ {
if ((int)Build.VERSION.SdkInt < 33)
{
// API 33 required to avoid using wildcard app visibility or dangerous permissions
// https://developer.android.com/reference/android/content/pm/PackageManager#getLaunchIntentSenderForPackage(java.lang.String)
return false;
}
var activity = CrossCurrentActivity.Current.Activity; var activity = CrossCurrentActivity.Current.Activity;
appName = appName.Replace("androidapp://", string.Empty); appName = appName.Replace("androidapp://", string.Empty);
var launchIntent = activity.PackageManager.GetLaunchIntentForPackage(appName); var launchIntentSender = activity?.PackageManager?.GetLaunchIntentSenderForPackage(appName);
if (launchIntent != null) launchIntentSender?.SendIntent(activity, Result.Ok, null, null, null);
{ return launchIntentSender != null;
activity.StartActivity(launchIntent);
}
return launchIntent != null;
} }
public async Task ShowLoadingAsync(string text) public async Task ShowLoadingAsync(string text)