implement support for MEGA, closes #99
This commit is contained in:
		| @@ -246,7 +246,7 @@ namespace keepass2android.Io | ||||
| 			else | ||||
| 			{ | ||||
| 				Intent intent = new Intent(); | ||||
| 				activity.IocToIntent(intent, new IOConnectionInfo() { Path = protocolId+"://"}); | ||||
| 				activity.IocToIntent(intent, new IOConnectionInfo() { Path = protocolId+"://", }); | ||||
| 				activity.OnImmediateResult(requestCode, (int) FileStorageResults.FileChooserPrepared, intent); | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										506
									
								
								src/Kp2aBusinessLogic/Io/MegaFileStorage.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								src/Kp2aBusinessLogic/Io/MegaFileStorage.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,506 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using Android.Content; | ||||
| using Android.OS; | ||||
| using Android.Preferences; | ||||
| using Android.Util; | ||||
| using CG.Web.MegaApiClient; | ||||
| using Group.Pals.Android.Lib.UI.Filechooser.Utils; | ||||
| using KeePassLib.Cryptography.Cipher; | ||||
| using KeePassLib.Serialization; | ||||
| using KeePassLib.Utility; | ||||
|  | ||||
| namespace keepass2android.Io | ||||
| { | ||||
|     public class MegaFileStorage : IFileStorage | ||||
|     { | ||||
|         private readonly Context _appContext; | ||||
|         public const string ProtocolId = "mega"; | ||||
|         private const string PreferenceKey = "KP2A-Mega-Accounts"; | ||||
|  | ||||
|         public MegaFileStorage(Context appContext) | ||||
|         { | ||||
|             _appContext = appContext; | ||||
|         } | ||||
|  | ||||
|         //we don't want to store passwords in plain text, encrypt them with this key at least: | ||||
|         public static readonly byte[] EncryptionKey = new byte[] { 86,239,128,218,160,22,245,114,193,92,151,10,134,104,121,170, | ||||
|             183,110,60,38,179,181,24,206,169,43,125,193,142,156,47,45}; | ||||
|  | ||||
|         public class AccountSettings | ||||
|         { | ||||
|             public Dictionary<string, string> PasswordByUsername { get; set; } = new Dictionary<string, string>(); | ||||
|  | ||||
|             public static byte[] exclusiveOR(byte[] arr1, byte[] arr2) | ||||
|             { | ||||
|                 byte[] result = new byte[arr1.Length]; | ||||
|  | ||||
|                 for (int i = 0; i < arr1.Length; ++i) | ||||
|                     result[i] = (byte)(arr1[i] ^ arr2[i % arr2.Length]); | ||||
|  | ||||
|                 return result; | ||||
|             } | ||||
|  | ||||
|             static string Encrypt(string s) | ||||
|             { | ||||
|                 var plainTextBytes = exclusiveOR(System.Text.Encoding.UTF8.GetBytes(s), EncryptionKey); | ||||
|                 return System.Convert.ToBase64String(plainTextBytes); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             static string Decrypt(string s) | ||||
|             { | ||||
|                 var base64EncodedBytes = System.Convert.FromBase64String(s); | ||||
|                 return System.Text.Encoding.UTF8.GetString(exclusiveOR(base64EncodedBytes, EncryptionKey)); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             public string Serialize() | ||||
|             { | ||||
|                 Dictionary<string, string> encryptedPasswordByUsername = PasswordByUsername | ||||
|                     .Select(kvp => new KeyValuePair<string, string>(kvp.Key, Encrypt(kvp.Value))) | ||||
|                     .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | ||||
|                 return Newtonsoft.Json.JsonConvert.SerializeObject(encryptedPasswordByUsername); | ||||
|             } | ||||
|  | ||||
|             public void Deserialize(string data) | ||||
|             { | ||||
|                 if (string.IsNullOrEmpty(data)) | ||||
|                 { | ||||
|                     PasswordByUsername = new Dictionary<string, string>(); | ||||
|                     return; | ||||
|                 } | ||||
|                 Dictionary<string, string> encryptedPasswordByUsername = | ||||
|                     Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(data); | ||||
|                 PasswordByUsername = encryptedPasswordByUsername | ||||
|                     .Select(kvp => new KeyValuePair<string, string>(kvp.Key, Decrypt(kvp.Value))) | ||||
|                     .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public IEnumerable<string> SupportedProtocols | ||||
|         { | ||||
|             get { yield return ProtocolId; } | ||||
|         } | ||||
|  | ||||
|         public bool UserShouldBackup | ||||
|         { | ||||
|             get { return false; } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         class MegaFileStorageWriteTransaction : IWriteTransaction | ||||
|         { | ||||
|             public bool UseFileTransaction { get; } | ||||
|             private readonly string _path; | ||||
|             private readonly MegaFileStorage _filestorage; | ||||
|             private MemoryStream _memoryStream; | ||||
|  | ||||
|             public MegaFileStorageWriteTransaction(string path, MegaFileStorage filestorage, bool useFileTransaction) | ||||
|             { | ||||
|                 UseFileTransaction = useFileTransaction; | ||||
|                 _path = path; | ||||
|                 _filestorage = filestorage; | ||||
|             } | ||||
|  | ||||
|             public void Dispose() | ||||
|             { | ||||
|                 _memoryStream.Dispose(); | ||||
|             } | ||||
|  | ||||
|             public Stream OpenFile() | ||||
|             { | ||||
|                 _memoryStream = new MemoryStream(); | ||||
|                 return _memoryStream; | ||||
|             } | ||||
|  | ||||
|             public void CommitWrite() | ||||
|             { | ||||
|                 _filestorage.UploadFile(_path, new MemoryStream(_memoryStream.ToArray()), UseFileTransaction); | ||||
|  | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void UploadFile(string path, MemoryStream memoryStream, bool useTransaction) | ||||
|         { | ||||
|             var accountData = GetAccountData(path); | ||||
|  | ||||
|             if (accountData.TryGetNode(path, out var node)) | ||||
|             { | ||||
|                 if (useTransaction) | ||||
|                 { | ||||
|                     string temporaryName = node.Name + "." + new Guid().ToString() + ".tmp"; | ||||
|                     var newNode = accountData.Client.Upload(memoryStream, temporaryName, accountData.GetParentNode(node)); | ||||
|                     accountData.Client.Delete(node); | ||||
|                     newNode = accountData.Client.Rename(newNode, node.Name); | ||||
|                     accountData._nodes.Remove(node); | ||||
|                     accountData._nodes.Add(newNode); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     var newNode = accountData.Client.Upload(memoryStream, node.Name, accountData.GetParentNode(node)); | ||||
|                     //we now have two nodes with the same name. Delete the old one: | ||||
|                     accountData.Client.Delete(node); | ||||
|                     accountData._nodes.Remove(node); | ||||
|                     accountData._nodes.Add(newNode); | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 //file did not exist yet | ||||
|                 string parentPath = GetParentPath(new IOConnectionInfo() { Path = path }).Path; | ||||
|                 string name = path.Substring(parentPath.Length + 1); | ||||
|                 var newNode = accountData.Client.Upload(memoryStream, name, accountData.GetNode(parentPath)); | ||||
|                 accountData._nodes.Add(newNode); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, | ||||
|             string protocolId) | ||||
|         { | ||||
|             activity.PerformManualFileSelect(isForSave, requestCode, protocolId); | ||||
|         } | ||||
|  | ||||
|         public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public void OnResume(IFileStorageSetupActivity activity) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public void OnStart(IFileStorageSetupActivity activity) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) | ||||
|         { | ||||
|             return new MegaFileStorageWriteTransaction(ioc.Path, this, useFileTransaction); | ||||
|         } | ||||
|  | ||||
|         public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return UrlUtil.StripExtension( | ||||
|                 UrlUtil.GetFileName(ioc.Path)); | ||||
|         } | ||||
|  | ||||
|         public string GetFileExtension(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return UrlUtil.GetExtension(ioc.Path); | ||||
|         } | ||||
|  | ||||
|         public string CreateFilePath(string parent, string newFilename) | ||||
|         { | ||||
|             if (!parent.EndsWith("/")) | ||||
|                 parent += "/"; | ||||
|             return parent + newFilename; | ||||
|         } | ||||
|  | ||||
|         public bool IsReadOnly(IOConnectionInfo ioc, OptionalOut<UiStringKey> reason = null) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         public bool IsPermanentLocation(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename) | ||||
|         { | ||||
|             IOConnectionInfo res = folderPath.CloneDeep(); | ||||
|             if (!res.Path.EndsWith("/")) | ||||
|                 res.Path += "/"; | ||||
|             res.Path += filename; | ||||
|             return res; | ||||
|         } | ||||
|  | ||||
|         public IOConnectionInfo GetParentPath(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return IoUtil.GetParentPath(ioc); | ||||
|         } | ||||
|  | ||||
|         public string GetDisplayName(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return ioc.GetDisplayName(); | ||||
|         } | ||||
|  | ||||
|         public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc) | ||||
|         { | ||||
|             //nothing to do | ||||
|         } | ||||
|  | ||||
|         public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, | ||||
|             bool alwaysReturnSuccess) | ||||
|         { | ||||
|             Intent intent = new Intent(); | ||||
|             activity.IocToIntent(intent, ioc); | ||||
|             activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileUsagePrepared, intent); | ||||
|         } | ||||
|  | ||||
|         public string IocToPath(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return ioc.Path; | ||||
|         } | ||||
|  | ||||
|         public bool RequiresSetup(IOConnectionInfo ioConnection) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         public FileDescription GetFileDescription(IOConnectionInfo ioc) | ||||
|         { | ||||
|             var accountData = GetAccountData(ioc); | ||||
|             return MakeFileDescription(accountData, accountData.GetNode(ioc)); | ||||
|         } | ||||
|  | ||||
|         class AccountData | ||||
|         { | ||||
|             public string Account { get; set; } | ||||
|             public IMegaApiClient Client { get; set; } | ||||
|  | ||||
|             public void RefreshMetadata() | ||||
|             { | ||||
|                 //make sure we refresh meta data after one minute: | ||||
|                 if (DateTime.Now.Subtract(_nodesLoadingTime).TotalMinutes > 1.0) | ||||
|                 { | ||||
|                     _nodes.Clear(); | ||||
|                     EnsureMetadataLoaded(); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             public List<INode> _nodes = new List<INode>(); | ||||
|             private DateTime _nodesLoadingTime; | ||||
|             private INode _rootNode; | ||||
|  | ||||
|             public INode GetNode(IOConnectionInfo ioc) | ||||
|             { | ||||
|                 return GetNode(ioc.Path); | ||||
|             } | ||||
|  | ||||
|             public bool TryGetNode(string path, out INode node) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     node = GetNode(path); | ||||
|                     return true; | ||||
|                 } | ||||
|                 catch (Exception e) | ||||
|                 { | ||||
|                     node = null; | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             public INode GetNode(string path) | ||||
|             { | ||||
|                 EnsureMetadataLoaded(); | ||||
|                 if (!path.StartsWith(ProtocolId + "://")) | ||||
|                     throw new Exception("Invalid Mega URL: " + path); | ||||
|                 path = path.Substring(ProtocolId.Length + 3); | ||||
|                 var parts = path.Split('/'); | ||||
|                 if (parts.Length < 1 || parts[0] == "") | ||||
|                     throw new Exception("Invalid Mega URL: " + path); | ||||
|  | ||||
|                 INode node = _rootNode; | ||||
|                 for (int i = 1; i < parts.Length; i++) | ||||
|                 { | ||||
|                     if (parts[i] == "") | ||||
|                         continue; | ||||
|                     var matchingChildren = _nodes.Where(n => n.ParentId == node.Id && n.Name == parts[i]).ToList(); | ||||
|                     if (matchingChildren.Count == 0) | ||||
|                         throw new FileNotFoundException("Did not find " + path); | ||||
|                     if (matchingChildren.Count > 1) | ||||
|                         throw new Java.IO.FileNotFoundException( | ||||
|                             $"Found more than one child with name {parts[i]} while trying to get node for {path}"); | ||||
|                     node = matchingChildren.Single(); | ||||
|                 } | ||||
|  | ||||
|                 return node; | ||||
|  | ||||
|             } | ||||
|  | ||||
|             private void EnsureMetadataLoaded() | ||||
|             { | ||||
|                 if (_nodes.Any() == false) | ||||
|                 { | ||||
|                     _nodes = Client.GetNodes().ToList(); | ||||
|  | ||||
|                     _rootNode = _nodes.Single(n => n.Type == NodeType.Root); | ||||
|                     _nodesLoadingTime = DateTime.Now; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             public INode GetParentNode(INode node) | ||||
|             { | ||||
|                 return _nodes.Single(n => n.Id == node.ParentId); | ||||
|             } | ||||
|  | ||||
|             internal void InvalidateMetaData() | ||||
|             { | ||||
|                 _nodes.Clear(); | ||||
|             } | ||||
|  | ||||
|             public IEnumerable<INode> GetChildNodes(INode node) | ||||
|             { | ||||
|                 EnsureMetadataLoaded(); | ||||
|                 return _nodes.Where(n => n.ParentId == node.Id); | ||||
|             } | ||||
|  | ||||
|             public string GetPath(INode node) | ||||
|             { | ||||
|                 if (node.Type == NodeType.Root) | ||||
|                     return ProtocolId + "://" + this.Account; | ||||
|                 var parent = _nodes.Single(n => n.Id == node.ParentId); | ||||
|                 return GetPath(parent) + "/" + node.Name; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         readonly Dictionary<string /*account*/, AccountData> _allAccountData = new Dictionary<string, AccountData>(); | ||||
|  | ||||
|         public string GetAccount(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return GetAccount(ioc.Path); | ||||
|         } | ||||
|  | ||||
|         public static string GetAccount(string path) | ||||
|         { | ||||
|             if (!path.StartsWith(ProtocolId + "://")) | ||||
|                 throw new Exception("Invalid Mega URL: " + path); | ||||
|             path = path.Substring(ProtocolId.Length + 3); | ||||
|             var parts = path.Split('/'); | ||||
|             if (parts.Length < 1 || parts[0] == "") | ||||
|                 throw new Exception("Invalid Mega URL: " + path); | ||||
|             return parts[0]; | ||||
|  | ||||
|  | ||||
|         } | ||||
|  | ||||
|         private AccountData GetAccountData(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return GetAccountData(ioc.Path); | ||||
|         } | ||||
|  | ||||
|         public static AccountSettings GetAccountSettings(Context ctx) | ||||
|         { | ||||
|             string accountSettingsString = PreferenceManager.GetDefaultSharedPreferences(ctx).GetString(PreferenceKey, null); | ||||
|             AccountSettings settings = new AccountSettings(); | ||||
|             settings.Deserialize(accountSettingsString); | ||||
|             return settings; | ||||
|         } | ||||
|  | ||||
|         public static void UpdateAccountSettings(AccountSettings settings, Context ctx) | ||||
|         { | ||||
|             PreferenceManager.GetDefaultSharedPreferences(ctx).Edit().PutString(PreferenceKey, settings.Serialize()) | ||||
|                 .Commit(); | ||||
|         } | ||||
|  | ||||
|         private AccountData GetAccountData(string path) | ||||
|         { | ||||
|             string account = GetAccount(path); | ||||
|             if (_allAccountData.TryGetValue(account, out var accountData)) | ||||
|             { | ||||
|                 return accountData; | ||||
|             } | ||||
|  | ||||
|             AccountData newAccountData = new AccountData() | ||||
|             { | ||||
|                 Account = account, | ||||
|                 Client = new MegaApiClient() | ||||
|             }; | ||||
|  | ||||
|             var settings = GetAccountSettings(_appContext); | ||||
|             if (!settings.PasswordByUsername.TryGetValue(account, out string password)) | ||||
|             { | ||||
|                 throw new Exception("No account configured with username = " + account); | ||||
|             } | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 newAccountData.Client.Login(account, password); | ||||
|             } | ||||
|             catch (CG.Web.MegaApiClient.ApiException e) | ||||
|             { | ||||
|                 if (e.ApiResultCode == CG.Web.MegaApiClient.ApiResultCode.ResourceNotExists) | ||||
|                 { | ||||
|                     throw new Exception("Failed to login to MEGA account. Please check username and password!"); | ||||
|                 } | ||||
|                      | ||||
|             } | ||||
|              | ||||
|  | ||||
|             _allAccountData[account] = newAccountData; | ||||
|             return newAccountData; | ||||
|  | ||||
|         } | ||||
|  | ||||
|         public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc) | ||||
|         { | ||||
|             AccountData accountData = GetAccountData(ioc); | ||||
|             accountData.RefreshMetadata(); | ||||
|             return accountData.GetChildNodes(accountData.GetNode(ioc)).Select(n => MakeFileDescription(accountData, n)); | ||||
|  | ||||
|  | ||||
|         } | ||||
|  | ||||
|         private FileDescription MakeFileDescription(AccountData account, INode n) | ||||
|         { | ||||
|             return new FileDescription() | ||||
|             { | ||||
|                 CanRead = true, | ||||
|                 CanWrite = true, | ||||
|                 DisplayName = n.Name ?? (n.Type == NodeType.Root ? "root" : ""), | ||||
|                 IsDirectory = n.Type != NodeType.File, | ||||
|                 LastModified = n.ModificationDate ?? n.CreationDate ?? DateTime.MinValue, | ||||
|                 Path = account.GetPath(n), | ||||
|                 SizeInBytes = n.Size | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         public void CreateDirectory(IOConnectionInfo ioc, string newDirName) | ||||
|         { | ||||
|             var accountData = GetAccountData(ioc); | ||||
|             var newNode = accountData.Client.CreateFolder(newDirName, accountData.GetNode(ioc)); | ||||
|             accountData._nodes.Add(newNode); | ||||
|         } | ||||
|  | ||||
|         public bool RequiresCredentials(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|  | ||||
|         public Stream OpenFileForRead(IOConnectionInfo ioc) | ||||
|         { | ||||
|             var accountData = GetAccountData(ioc); | ||||
|             return accountData.Client.Download(accountData.GetNode(ioc)); | ||||
|         } | ||||
|  | ||||
|         public string GetCurrentFileVersionFast(IOConnectionInfo ioc) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         public void Delete(IOConnectionInfo ioc) | ||||
|         { | ||||
|             var accountData = GetAccountData(ioc); | ||||
|             accountData.Client.Delete(accountData.GetNode(ioc)); | ||||
|         } | ||||
|  | ||||
|          | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Philipp Crocoll
					Philipp Crocoll