diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 4825654e1..73a19d10e 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -15,7 +15,7 @@ Properties\AndroidManifest.xml Resources Assets - v12.1 + v13.0 Xamarin.Android.Net.AndroidClientHandler @@ -77,12 +77,12 @@ 1.9.0 - - - - - - + + + + + + 1.7.3 @@ -103,8 +103,10 @@ + + diff --git a/src/Android/Autofill/AutofillConstants.cs b/src/Android/Autofill/AutofillConstants.cs new file mode 100644 index 000000000..9bb1782f7 --- /dev/null +++ b/src/Android/Autofill/AutofillConstants.cs @@ -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"; + } +} diff --git a/src/Android/Autofill/AutofillExternalSelectionActivity.cs b/src/Android/Autofill/AutofillExternalSelectionActivity.cs new file mode 100644 index 000000000..c75d150be --- /dev/null +++ b/src/Android/Autofill/AutofillExternalSelectionActivity.cs @@ -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(); + var cipher = await cipherService.GetAsync(cipherId); + var decCipher = await cipher.DecryptAsync(); + + var autofillHandler = ServiceContainer.Resolve(); + autofillHandler.Autofill(decCipher); + } + } +} diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs index f1984dbff..4702af3ac 100644 --- a/src/Android/Autofill/AutofillHelpers.cs +++ b/src/Android/Autofill/AutofillHelpers.cs @@ -207,7 +207,7 @@ namespace Bit.Droid.Autofill } } var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, items[i], - inlinePresentationSpec); + true, inlinePresentationSpec); if (dataset != null) { responseBuilder.AddDataset(dataset); @@ -221,7 +221,7 @@ namespace Bit.Droid.Autofill } public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem, - InlinePresentationSpec inlinePresentationSpec = null) + bool includeAuthIntent, InlinePresentationSpec inlinePresentationSpec = null) { var overlayPresentation = BuildOverlayPresentation( filledItem.Name, @@ -242,6 +242,15 @@ namespace Bit.Droid.Autofill { 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)) { return datasetBuilder.Build(); @@ -253,25 +262,26 @@ namespace Bit.Droid.Autofill IList inlinePresentationSpecs = null) { var intent = new Intent(context, typeof(MainActivity)); - intent.PutExtra("autofillFramework", true); + intent.PutExtra(AutofillConstants.AutofillFramework, true); if (fields.FillableForLogin) { - intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Login); + intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Login); } else if (fields.FillableForCard) { - intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Card); + intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Card); } else if (fields.FillableForIdentity) { - intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Identity); + intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Identity); } else { return null; } - intent.PutExtra("autofillFrameworkUri", uri); - var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true)); + intent.PutExtra(AutofillConstants.AutofillFrameworkUri, uri); + var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, + AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true)); var overlayPresentation = BuildOverlayPresentation( AppResources.AutofillWithBitwarden, diff --git a/src/Android/Autofill/FilledItem.cs b/src/Android/Autofill/FilledItem.cs index 3964f37bd..46b0436fc 100644 --- a/src/Android/Autofill/FilledItem.cs +++ b/src/Android/Autofill/FilledItem.cs @@ -23,6 +23,7 @@ namespace Bit.Droid.Autofill public FilledItem(CipherView cipher) { + Id = cipher.Id; Name = cipher.Name; Type = cipher.Type; Subtitle = cipher.SubTitle; @@ -55,6 +56,7 @@ namespace Bit.Droid.Autofill } } + public string Id { get; set; } public string Name { get; set; } public string Subtitle { get; set; } = string.Empty; public int Icon { get; set; } = Resource.Drawable.login; diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index cffc12ac0..2e25bc1cc 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -18,6 +18,7 @@ using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Utilities; +using Bit.Droid.Autofill; using Bit.Droid.Receivers; using Bit.Droid.Utilities; using Newtonsoft.Json; @@ -322,13 +323,13 @@ namespace Bit.Droid { var options = new AppOptions { - Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"), + Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri), MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false), GeneratorTile = Intent.GetBooleanExtra("generatorTile", false), - FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false), + FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false), CreateSend = GetCreateSendRequest(Intent) }; - var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0); + var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0); if (fillType > 0) { options.FillType = (CipherType)fillType; diff --git a/src/Android/Properties/AndroidManifest.xml b/src/Android/Properties/AndroidManifest.xml index ea7066845..54ed96ab6 100644 --- a/src/Android/Properties/AndroidManifest.xml +++ b/src/Android/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - + @@ -40,10 +40,4 @@ - - - - - - \ No newline at end of file diff --git a/src/Android/Receivers/ClearClipboardAlarmReceiver.cs b/src/Android/Receivers/ClearClipboardAlarmReceiver.cs index ff1566134..6e57cedb7 100644 --- a/src/Android/Receivers/ClearClipboardAlarmReceiver.cs +++ b/src/Android/Receivers/ClearClipboardAlarmReceiver.cs @@ -1,4 +1,5 @@ using Android.Content; +using Android.OS; namespace Bit.Droid.Receivers { @@ -8,7 +9,17 @@ namespace Bit.Droid.Receivers public override void OnReceive(Context context, Intent intent) { var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager; - clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " "); + 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", " "); + return; + } + clipboardManager.ClearPrimaryClip(); } } } diff --git a/src/Android/Services/AutofillHandler.cs b/src/Android/Services/AutofillHandler.cs index 9cfa5ec9e..ad1a0ad5d 100644 --- a/src/Android/Services/AutofillHandler.cs +++ b/src/Android/Services/AutofillHandler.cs @@ -73,12 +73,12 @@ namespace Bit.Droid.Services 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) { return; } - if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false) + if (activity.Intent?.GetBooleanExtra(AutofillConstants.AutofillFramework, false) ?? false) { if (cipher == null) { @@ -103,7 +103,7 @@ namespace Bit.Droid.Services return; } 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(); replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset); activity.SetResult(Result.Ok, replyIntent); diff --git a/src/Android/Services/ClipboardService.cs b/src/Android/Services/ClipboardService.cs index 33585a274..fbb7a1ffb 100644 --- a/src/Android/Services/ClipboardService.cs +++ b/src/Android/Services/ClipboardService.cs @@ -6,7 +6,6 @@ using Android.OS; using Bit.Core.Abstractions; using Bit.Droid.Receivers; using Bit.Droid.Utilities; -using Plugin.CurrentActivity; using Xamarin.Essentials; namespace Bit.Droid.Services @@ -21,9 +20,9 @@ namespace Bit.Droid.Services _stateService = stateService; _clearClipboardPendingIntent = new Lazy(() => - PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity, + PendingIntent.GetBroadcast(Application.Context, 0, - new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)), + new Intent(Application.Context, typeof(ClearClipboardAlarmReceiver)), 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")) { - // #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. } } @@ -58,9 +57,7 @@ namespace Bit.Droid.Services private void CopyToClipboard(string text, bool isSensitive = true) { - var activity = (MainActivity)CrossCurrentActivity.Current.Activity; - var clipboardManager = activity.GetSystemService( - Context.ClipboardService) as Android.Content.ClipboardManager; + var clipboardManager = Application.Context.GetSystemService(Context.ClipboardService) as ClipboardManager; var clipData = ClipData.NewPlainText("bitwarden", text); if (isSensitive) { @@ -87,7 +84,7 @@ namespace Bit.Droid.Services return; } 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); } } diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index c50558280..f189c77ce 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -69,14 +69,17 @@ namespace Bit.Droid.Services 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; appName = appName.Replace("androidapp://", string.Empty); - var launchIntent = activity.PackageManager.GetLaunchIntentForPackage(appName); - if (launchIntent != null) - { - activity.StartActivity(launchIntent); - } - return launchIntent != null; + var launchIntentSender = activity?.PackageManager?.GetLaunchIntentSenderForPackage(appName); + launchIntentSender?.SendIntent(activity, Result.Ok, null, null, null); + return launchIntentSender != null; } public async Task ShowLoadingAsync(string text)