diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs
index 6c3f2dd3f..89a8cbddf 100644
--- a/src/Android/MainApplication.cs
+++ b/src/Android/MainApplication.cs
@@ -46,7 +46,6 @@ namespace Bit.Droid
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
liteDbStorage.InitAsync();
- var deviceActionService = new DeviceActionService();
var localizeService = new LocalizeService();
var broadcasterService = new BroadcasterService();
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
@@ -54,6 +53,7 @@ namespace Bit.Droid
var secureStorageService = new SecureStorageService();
var cryptoPrimitiveService = new CryptoPrimitiveService();
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
+ var deviceActionService = new DeviceActionService(mobileStorageService);
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
broadcasterService);
diff --git a/src/Android/Properties/AndroidManifest.xml b/src/Android/Properties/AndroidManifest.xml
index 51be9590d..bdf3d06f3 100644
--- a/src/Android/Properties/AndroidManifest.xml
+++ b/src/Android/Properties/AndroidManifest.xml
@@ -26,5 +26,14 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config">
+
+
+
diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs
index fd01b3e66..cd5aa51a3 100644
--- a/src/Android/Services/DeviceActionService.cs
+++ b/src/Android/Services/DeviceActionService.cs
@@ -1,6 +1,14 @@
-using System.Threading.Tasks;
+using System;
+using System.IO;
+using System.Threading.Tasks;
using Android.App;
+using Android.Content;
+using Android.Content.PM;
+using Android.Support.V4.Content;
+using Android.Webkit;
using Bit.App.Abstractions;
+using Bit.Core;
+using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Plugin.CurrentActivity;
@@ -8,9 +16,16 @@ namespace Bit.Droid.Services
{
public class DeviceActionService : IDeviceActionService
{
+ private readonly IStorageService _storageService;
+
private ProgressDialog _progressDialog;
private Android.Widget.Toast _toast;
+ public DeviceActionService(IStorageService storageService)
+ {
+ _storageService = storageService;
+ }
+
public DeviceType DeviceType => DeviceType.Android;
public void Toast(string text, bool longDuration = false)
@@ -61,5 +76,100 @@ namespace Bit.Droid.Services
}
return Task.FromResult(0);
}
+
+ public bool OpenFile(byte[] fileData, string id, string fileName)
+ {
+ if(!CanOpenFile(fileName))
+ {
+ return false;
+ }
+ var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
+ if(extension == null)
+ {
+ return false;
+ }
+ var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
+ if(mimeType == null)
+ {
+ return false;
+ }
+
+ var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
+ var cachePath = activity.CacheDir;
+ var filePath = Path.Combine(cachePath.Path, fileName);
+ File.WriteAllBytes(filePath, fileData);
+ var file = new Java.IO.File(cachePath, fileName);
+ if(!file.IsFile)
+ {
+ return false;
+ }
+
+ try
+ {
+ var intent = new Intent(Intent.ActionView);
+ var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
+ "com.x8bit.bitwarden.fileprovider", file);
+ intent.SetDataAndType(uri, mimeType);
+ intent.SetFlags(ActivityFlags.GrantReadUriPermission);
+ activity.StartActivity(intent);
+ return true;
+ }
+ catch { }
+ return false;
+ }
+
+ public bool CanOpenFile(string fileName)
+ {
+ var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
+ if(extension == null)
+ {
+ return false;
+ }
+ var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
+ if(mimeType == null)
+ {
+ return false;
+ }
+ var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
+ var intent = new Intent(Intent.ActionView);
+ intent.SetType(mimeType);
+ var activities = activity.PackageManager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
+ return (activities?.Count ?? 0) > 0;
+ }
+
+ public async Task ClearCacheAsync()
+ {
+ try
+ {
+ DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
+ await _storageService.SaveAsync(Constants.LastFileCacheClearKey, DateTime.UtcNow);
+ }
+ catch(Exception) { }
+ }
+
+ private bool DeleteDir(Java.IO.File dir)
+ {
+ if(dir != null && dir.IsDirectory)
+ {
+ var children = dir.List();
+ for(int i = 0; i < children.Length; i++)
+ {
+ var success = DeleteDir(new Java.IO.File(dir, children[i]));
+ if(!success)
+ {
+ return false;
+ }
+ }
+ return dir.Delete();
+ }
+ else if(dir != null && dir.IsFile)
+ {
+ return dir.Delete();
+ }
+ else
+ {
+ return false;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs
index 7b4554ad1..201225a4b 100644
--- a/src/App/Abstractions/IDeviceActionService.cs
+++ b/src/App/Abstractions/IDeviceActionService.cs
@@ -10,5 +10,8 @@ namespace Bit.App.Abstractions
bool LaunchApp(string appName);
Task ShowLoadingAsync(string text);
Task HideLoadingAsync();
+ bool OpenFile(byte[] fileData, string id, string fileName);
+ bool CanOpenFile(string fileName);
+ Task ClearCacheAsync();
}
}
\ No newline at end of file
diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs
index 66c868988..3b502f05b 100644
--- a/src/App/Pages/Vault/ViewPageViewModel.cs
+++ b/src/App/Pages/Vault/ViewPageViewModel.cs
@@ -73,6 +73,7 @@ namespace Bit.App.Pages
nameof(IsSecureNote),
nameof(ShowUris),
nameof(ShowFields),
+ nameof(ShowAttachments),
nameof(ShowTotp),
nameof(ColoredPassword),
nameof(ShowIdentityAddress),
@@ -253,10 +254,44 @@ namespace Bit.App.Pages
if(Cipher.OrganizationId == null && !CanAccessPremium)
{
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
+ return;
}
+ if(attachment.FileSize >= 10485760) // 10 MB
+ {
+ var confirmed = await _platformUtilsService.ShowDialogAsync(
+ string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
+ AppResources.Yes, AppResources.No);
+ if(!confirmed)
+ {
+ return;
+ }
+ }
+ if(!_deviceActionService.CanOpenFile(attachment.FileName))
+ {
+ await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
+ return;
+ }
+
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
- await Task.Delay(2000); // TODO: download
- await _deviceActionService.HideLoadingAsync();
+ try
+ {
+ var data = await _cipherService.DownloadAndDecryptAttachmentAsync(attachment, Cipher.OrganizationId);
+ await _deviceActionService.HideLoadingAsync();
+ if(data == null)
+ {
+ await _platformUtilsService.ShowDialogAsync(AppResources.UnableToDownloadFile);
+ return;
+ }
+ if(!_deviceActionService.OpenFile(data, attachment.Id, attachment.FileName))
+ {
+ await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
+ return;
+ }
+ }
+ catch
+ {
+ await _deviceActionService.HideLoadingAsync();
+ }
}
private async void CopyAsync(string id, string text = null)
diff --git a/src/Core/Abstractions/ICipherService.cs b/src/Core/Abstractions/ICipherService.cs
index c41af500c..ff6f31c09 100644
--- a/src/Core/Abstractions/ICipherService.cs
+++ b/src/Core/Abstractions/ICipherService.cs
@@ -35,5 +35,6 @@ namespace Bit.Core.Abstractions
Task UpdateLastUsedDateAsync(string id);
Task UpsertAsync(CipherData cipher);
Task UpsertAsync(List cipher);
+ Task DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId);
}
}
\ No newline at end of file
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index 42b0d6087..958ee0feb 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -9,5 +9,6 @@
public static string DefaultUriMatch = "defaultUriMatch";
public static string DisableAutoTotpCopyKey = "disableAutoTotpCopy";
public static string EnvironmentUrlsKey = "environmentUrls";
+ public static string LastFileCacheClearKey = "lastFileCacheClear";
}
}
diff --git a/src/Core/Models/View/AttachmentView.cs b/src/Core/Models/View/AttachmentView.cs
index b97dfdcfa..a0782d4fb 100644
--- a/src/Core/Models/View/AttachmentView.cs
+++ b/src/Core/Models/View/AttachmentView.cs
@@ -20,5 +20,17 @@ namespace Bit.Core.Models.View
public string SizeName { get; set; }
public string FileName { get; set; }
public SymmetricCryptoKey Key { get; set; }
+
+ public long FileSize
+ {
+ get
+ {
+ if(!string.IsNullOrWhiteSpace(Size) && long.TryParse(Size, out var s))
+ {
+ return s;
+ }
+ return 0;
+ }
+ }
}
}
diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs
index e8a98601b..fd1f54cbe 100644
--- a/src/Core/Services/CipherService.cs
+++ b/src/Core/Services/CipherService.cs
@@ -678,6 +678,27 @@ namespace Bit.Core.Services
await DeleteAttachmentAsync(id, attachmentId);
}
+ public async Task DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId)
+ {
+ try
+ {
+ var response = await _httpClient.GetAsync(new Uri(attachment.Url));
+ if(!response.IsSuccessStatusCode)
+ {
+ return null;
+ }
+ var data = await response.Content.ReadAsByteArrayAsync();
+ if(data == null)
+ {
+ return null;
+ }
+ var key = attachment.Key ?? await _cryptoService.GetOrgKeyAsync(organizationId);
+ return await _cryptoService.DecryptFromBytesAsync(data, key);
+ }
+ catch { }
+ return null;
+ }
+
// Helpers
private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId,
diff --git a/src/iOS/Services/DeviceActionService.cs b/src/iOS/Services/DeviceActionService.cs
index a72c972b4..33bbf6367 100644
--- a/src/iOS/Services/DeviceActionService.cs
+++ b/src/iOS/Services/DeviceActionService.cs
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Bit.App.Abstractions;
+using Bit.Core;
+using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.iOS.Core.Views;
using CoreGraphics;
@@ -14,9 +17,16 @@ namespace Bit.iOS.Services
{
public class DeviceActionService : IDeviceActionService
{
+ private readonly IStorageService _storageService;
+
private Toast _toast;
private UIAlertController _progressAlert;
+ public DeviceActionService(IStorageService storageService)
+ {
+ _storageService = storageService;
+ }
+
public DeviceType DeviceType => DeviceType.iOS;
public bool LaunchApp(string appName)
@@ -82,6 +92,57 @@ namespace Bit.iOS.Services
return result.Task;
}
+ public bool OpenFile(byte[] fileData, string id, string fileName)
+ {
+ var filePath = Path.Combine(GetTempPath(), fileName);
+ File.WriteAllBytes(filePath, fileData);
+ var url = NSUrl.FromFilename(filePath);
+ var viewer = UIDocumentInteractionController.FromUrl(url);
+ var controller = GetVisibleViewController();
+ var rect = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad ?
+ new CGRect(100, 5, 320, 320) : controller.View.Frame;
+ return viewer.PresentOpenInMenu(rect, controller.View, true);
+ }
+
+ public bool CanOpenFile(string fileName)
+ {
+ // Not sure of a way to check this ahead of time on iOS
+ return true;
+ }
+
+ public async Task ClearCacheAsync()
+ {
+ var url = new NSUrl(GetTempPath());
+ var tmpFiles = NSFileManager.DefaultManager.GetDirectoryContent(url, null,
+ NSDirectoryEnumerationOptions.SkipsHiddenFiles, out NSError error);
+ if(error == null && tmpFiles.Length > 0)
+ {
+ foreach(var item in tmpFiles)
+ {
+ NSFileManager.DefaultManager.Remove(item, out NSError itemError);
+ }
+ }
+ await _storageService.SaveAsync(Constants.LastFileCacheClearKey, DateTime.UtcNow);
+ }
+
+ private UIViewController GetVisibleViewController(UIViewController controller = null)
+ {
+ controller = controller ?? UIApplication.SharedApplication.KeyWindow.RootViewController;
+ if(controller.PresentedViewController == null)
+ {
+ return controller;
+ }
+ if(controller.PresentedViewController is UINavigationController)
+ {
+ return ((UINavigationController)controller.PresentedViewController).VisibleViewController;
+ }
+ if(controller.PresentedViewController is UITabBarController)
+ {
+ return ((UITabBarController)controller.PresentedViewController).SelectedViewController;
+ }
+ return GetVisibleViewController(controller.PresentedViewController);
+ }
+
private UIViewController GetPresentedViewController()
{
var window = UIApplication.SharedApplication.KeyWindow;
@@ -99,5 +160,12 @@ namespace Bit.iOS.Services
return vc != null && (vc is UITabBarController ||
(vc.ChildViewControllers?.Any(c => c is UITabBarController) ?? false));
}
+
+ // ref: //https://developer.xamarin.com/guides/ios/application_fundamentals/working_with_the_file_system/
+ public string GetTempPath()
+ {
+ var documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
+ return Path.Combine(documents, "..", "tmp");
+ }
}
}
\ No newline at end of file