using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Cryptography; using System.Text; using Android.Content; using Android.Content.PM; using Android.OS; using KeePassLib.Cryptography; using KeePassLib.Serialization; using KeePassLib.Utility; namespace keepass2android.Io { /// /// Interface for classes which can handle certain Cache events on a higher level (e.g. by user interaction) /// public interface ICacheSupervisor { /// /// called when a save operation only updated the cache but not the remote file /// /// The file which we tried to write /// The exception why the remote file couldn't be updated void CouldntSaveToRemote(IOConnectionInfo ioc, Exception ex); /// /// Called when only the local file could be opened during an open operation. /// void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex); /// /// Called when the local file either didn't exist or was unmodified, so the remote file /// was loaded and the cache was updated during the load operation. /// void UpdatedCachedFileOnLoad(IOConnectionInfo ioc); /// /// Called when the remote file either didn't exist or was unmodified, so the local file /// was loaded and the remote file was updated during the load operation. /// void UpdatedRemoteFileOnLoad(IOConnectionInfo ioc); /// /// Called to notify the supervisor that the file described by ioc is opened from the cache because there's a conflict /// with local and remote changes /// /// void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc); /// /// Called when the load operation was performed and the remote file was identical with the local file /// void LoadedFromRemoteInSync(IOConnectionInfo ioc); } /// /// Implements the IFileStorage interface as a proxy: A base storage is used as a remote storage. Local files are used to cache the /// files on remote. /// public class CachingFileStorage : IFileStorage, IOfflineSwitchable, IPermissionRequestingFileStorage { protected readonly OfflineSwitchableFileStorage _cachedStorage; private readonly ICacheSupervisor _cacheSupervisor; private readonly string _streamCacheDir; public CachingFileStorage(IFileStorage cachedStorage, string cacheDir, ICacheSupervisor cacheSupervisor) { _cachedStorage = new OfflineSwitchableFileStorage(cachedStorage); _cacheSupervisor = cacheSupervisor; _streamCacheDir = cacheDir + Java.IO.File.Separator + "OfflineCache" + Java.IO.File.Separator; if (!Directory.Exists(_streamCacheDir)) Directory.CreateDirectory(_streamCacheDir); } public void ClearCache() { IoUtil.DeleteDir(new Java.IO.File(_streamCacheDir), true); } public IEnumerable SupportedProtocols { get { return _cachedStorage.SupportedProtocols; } } public void DeleteFile(IOConnectionInfo ioc) { if (IsCached(ioc)) { File.Delete(CachedFilePath(ioc)); File.Delete(VersionFilePath(ioc)); File.Delete(BaseVersionFilePath(ioc)); } _cachedStorage.Delete(ioc); } private string CachedFilePath(IOConnectionInfo ioc) { SHA256Managed sha256 = new SHA256Managed(); string iocAsHexString = MemUtil.ByteArrayToHexString(sha256.ComputeHash(Encoding.Unicode.GetBytes(ioc.Path.ToCharArray())))+".cache"; return _streamCacheDir + iocAsHexString; } public bool IsCached(IOConnectionInfo ioc) { return File.Exists(CachedFilePath(ioc)) && File.Exists(VersionFilePath(ioc)) && File.Exists(BaseVersionFilePath(ioc)); } public void Delete(IOConnectionInfo ioc) { _cachedStorage.Delete(ioc); } public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion) { //see comment in GetCurrentFileVersionFast return false; } public string GetCurrentFileVersionFast(IOConnectionInfo ioc) { //fast file version checking is not supported by CachingFileStorage: //it's hard to return good versions in cases that the base source is offline //or after modifying the cache. //It's probably not relevant because fast file version checking is meant for local storage //which is not cached. return String.Empty; } private string VersionFilePath(IOConnectionInfo ioc) { return CachedFilePath(ioc)+".version"; } private string BaseVersionFilePath(IOConnectionInfo ioc) { return CachedFilePath(ioc) + ".baseversion"; } public Stream OpenFileForRead(IOConnectionInfo ioc) { string cachedFilePath = CachedFilePath(ioc); try { if (!IsCached(ioc) || GetLocalVersionHash(ioc) == GetBaseVersionHash(ioc)) { Kp2aLog.Log("CFS: OpenWhenNoLocalChanges"); return OpenFileForReadWhenNoLocalChanges(ioc, cachedFilePath); } else { Kp2aLog.Log("CFS: OpenWhenLocalChanges"); return OpenFileForReadWhenLocalChanges(ioc, cachedFilePath); } } catch (Exception ex) { if (!IsCached(ioc)) throw; Kp2aLog.Log("couldn't open from remote " + ioc.Path); Kp2aLog.Log(ex.ToString()); _cacheSupervisor.CouldntOpenFromRemote(ioc, ex); return File.OpenRead(cachedFilePath); } } private Stream OpenFileForReadWhenLocalChanges(IOConnectionInfo ioc, string cachedFilePath) { //file is cached but has local modifications //try to upload the changes if remote file doesn't have changes as well: var hash = CalculateHash(ioc); if (File.ReadAllText(BaseVersionFilePath(ioc)) == hash) { Kp2aLog.Log("CFS: No changes in remote"); //no changes in remote file -> upload using (Stream localData = File.OpenRead(CachedFilePath(ioc))) { if (TryUpdateRemoteFile(localData, ioc, true, hash)) { _cacheSupervisor.UpdatedRemoteFileOnLoad(ioc); Kp2aLog.Log("CFS: Updated remote file"); } return File.OpenRead(cachedFilePath); } } else { Kp2aLog.Log("CFS: Files in conflict"); //conflict: both files changed. return OpenFileForReadWithConflict(ioc, cachedFilePath); } } protected virtual Stream OpenFileForReadWithConflict(IOConnectionInfo ioc, string cachedFilePath) { //signal that we're loading from local _cacheSupervisor.NotifyOpenFromLocalDueToConflict(ioc); return File.OpenRead(cachedFilePath); } public MemoryStream GetRemoteDataAndHash(IOConnectionInfo ioc, out string hash) { MemoryStream remoteData = new MemoryStream(); using (var remoteStream =_cachedStorage.OpenFileForRead(ioc)) { //note: directly copying to remoteData and hashing causes NullReferenceExceptions in FTP and with Digest auth // -> use the temp data approach: MemoryStream tempData = new MemoryStream(); remoteStream.CopyTo(tempData); tempData.Position = 0; HashingStreamEx hashingRemoteStream = new HashingStreamEx(tempData, false, new SHA256Managed()); hashingRemoteStream.CopyTo(remoteData); hashingRemoteStream.Close(); hash = MemUtil.ByteArrayToHexString(hashingRemoteStream.Hash); } remoteData.Position = 0; return remoteData; } private string CalculateHash(IOConnectionInfo ioc) { string hash; GetRemoteDataAndHash(ioc, out hash); return hash; } private Stream OpenFileForReadWhenNoLocalChanges(IOConnectionInfo ioc, string cachedFilePath) { //remember current hash string previousHash = null; string baseVersionFilePath = BaseVersionFilePath(ioc); if (File.Exists(baseVersionFilePath)) { Kp2aLog.Log("CFS: hashing cached version"); previousHash = File.ReadAllText(baseVersionFilePath); } //copy to cache: var fileHash = UpdateCacheFromRemote(ioc, cachedFilePath); //notify supervisor what we did: if (previousHash != fileHash) { Kp2aLog.Log("CFS: Updated Cache"); _cacheSupervisor.UpdatedCachedFileOnLoad(ioc); } else { Kp2aLog.Log("CFS: Files in Sync"); _cacheSupervisor.LoadedFromRemoteInSync(ioc); } return File.OpenRead(cachedFilePath); } /// /// copies the file in ioc to the local cache. Updates the cache version files and returns the new file hash. /// protected string UpdateCacheFromRemote(IOConnectionInfo ioc, string cachedFilePath) { //note: we might use the file version to check if it's already in the cache and if copying is required. //However, this is safer. string fileHash; //open stream: using (Stream remoteFile = _cachedStorage.OpenFileForRead(ioc)) { using (HashingStreamEx cachedFile = new HashingStreamEx(File.Create(cachedFilePath), true, new SHA256Managed())) { remoteFile.CopyTo(cachedFile); cachedFile.Close(); fileHash = MemUtil.ByteArrayToHexString(cachedFile.Hash); } } //save hash in cache files: File.WriteAllText(VersionFilePath(ioc), fileHash); File.WriteAllText(BaseVersionFilePath(ioc), fileHash); return fileHash; } private bool TryUpdateRemoteFile(Stream cachedData, IOConnectionInfo ioc, bool useFileTransaction, string hash) { try { UpdateRemoteFile(cachedData, ioc, useFileTransaction, hash); return true; } catch (Exception e) { Kp2aLog.Log("couldn't save to remote " + ioc.Path); Kp2aLog.Log(e.ToString()); //notify the supervisor so it might display a warning or schedule a retry _cacheSupervisor.CouldntSaveToRemote(ioc, e); return false; } } protected void UpdateRemoteFile(Stream cachedData, IOConnectionInfo ioc, bool useFileTransaction, string hash) { //try to write to remote: using ( IWriteTransaction remoteTrans = _cachedStorage.OpenWriteTransaction(ioc, useFileTransaction)) { Stream remoteStream = remoteTrans.OpenFile(); cachedData.CopyTo(remoteStream); remoteStream.Close(); remoteTrans.CommitWrite(); } //success. Update base-version of cache: File.WriteAllText(BaseVersionFilePath(ioc), hash); File.WriteAllText(VersionFilePath(ioc), hash); } public void UpdateRemoteFile(IOConnectionInfo ioc, bool useFileTransaction) { using (Stream cachedData = File.OpenRead(CachedFilePath(ioc))) { UpdateRemoteFile(cachedData, ioc, useFileTransaction, GetLocalVersionHash(ioc)); } } private class CachedWriteTransaction: IWriteTransaction { private class CachedWriteMemoryStream : MemoryStream { private readonly IOConnectionInfo ioc; private readonly CachingFileStorage _cachingFileStorage; private readonly bool _useFileTransaction; private bool _closed; public CachedWriteMemoryStream(IOConnectionInfo ioc, CachingFileStorage cachingFileStorage, bool useFileTransaction) { this.ioc = ioc; _cachingFileStorage = cachingFileStorage; _useFileTransaction = useFileTransaction; } public override void Close() { if (_closed) return; //write file to cache: //(note: this might overwrite local changes. It's assumed that a sync operation or check was performed before string hash; using (var hashingStream = new HashingStreamEx(File.Create(_cachingFileStorage.CachedFilePath(ioc)), true, new SHA256Managed())) { Position = 0; CopyTo(hashingStream); hashingStream.Close(); hash = MemUtil.ByteArrayToHexString(hashingStream.Hash); } File.WriteAllText(_cachingFileStorage.VersionFilePath(ioc), hash); //update file on remote. This might overwrite changes there as well, see above. Position = 0; if (_cachingFileStorage.IsCached(ioc)) { //if the file already is in the cache, it's ok if writing to remote fails. _cachingFileStorage.TryUpdateRemoteFile(this, ioc, _useFileTransaction, hash); } else { //if not, we don't accept a failure (e.g. invalid credentials would always remain a problem) _cachingFileStorage.UpdateRemoteFile(this, ioc, _useFileTransaction, hash); } base.Close(); _closed = true; } } private readonly IOConnectionInfo _ioc; private readonly bool _useFileTransaction; private readonly CachingFileStorage _cachingFileStorage; private MemoryStream _memoryStream; private bool _committed; public CachedWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction, CachingFileStorage cachingFileStorage) { _ioc = ioc; _useFileTransaction = useFileTransaction; _cachingFileStorage = cachingFileStorage; } public void Dispose() { if (!_committed) { try { _memoryStream.Dispose(); } catch (ObjectDisposedException e) { Kp2aLog.Log("Ignoring exception in Dispose: "+e); } } } public Stream OpenFile() { _memoryStream = new CachedWriteMemoryStream(_ioc, _cachingFileStorage, _useFileTransaction); return _memoryStream; } public void CommitWrite() { //the transaction is committed in the stream's Close _committed = true; } } public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) { //create a transaction which writes to memory stream //on close: write to cache. If possible, write to online //update versions return new CachedWriteTransaction(ioc, useFileTransaction, this); } public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) { return _cachedStorage.GetFilenameWithoutPathAndExt(ioc); } public bool RequiresCredentials(IOConnectionInfo ioc) { return _cachedStorage.RequiresCredentials(ioc); } public void CreateDirectory(IOConnectionInfo ioc, string newDirName) { _cachedStorage.CreateDirectory(ioc, newDirName); } public IEnumerable ListContents(IOConnectionInfo ioc) { return _cachedStorage.ListContents(ioc); } public FileDescription GetFileDescription(IOConnectionInfo ioc) { return _cachedStorage.GetFileDescription(ioc); } public bool RequiresSetup(IOConnectionInfo ioConnection) { return _cachedStorage.RequiresSetup(ioConnection); } public string IocToPath(IOConnectionInfo ioc) { return _cachedStorage.IocToPath(ioc); } public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId) { _cachedStorage.StartSelectFile(activity, isForSave, requestCode, protocolId); } public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, bool alwaysReturnSuccess) { //we try to prepare the file usage by the underlying file storage but if the ioc is cached, set the flag to ignore errors _cachedStorage.PrepareFileUsage(activity, ioc, requestCode, alwaysReturnSuccess || IsCached(ioc)); } public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc) { _cachedStorage.PrepareFileUsage(ctx, ioc); } public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState) { _cachedStorage.OnCreate(activity, savedInstanceState); } public void OnResume(IFileStorageSetupActivity activity) { _cachedStorage.OnResume(activity); } public void OnStart(IFileStorageSetupActivity activity) { _cachedStorage.OnStart(activity); } public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) { _cachedStorage.OnActivityResult(activity, requestCode, resultCode, data); } public string GetDisplayName(IOConnectionInfo ioc) { return _cachedStorage.GetDisplayName(ioc); } public string CreateFilePath(string parent, string newFilename) { return _cachedStorage.CreateFilePath(parent, newFilename); } public IOConnectionInfo GetParentPath(IOConnectionInfo ioc) { return _cachedStorage.GetParentPath(ioc); } public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename) { try { IOConnectionInfo res = _cachedStorage.GetFilePath(folderPath, filename); //some file storage implementations require accessing the network to determine the file path (e.g. because //they might contain file ids). In this case, we need to cache the result to enable cached access to such files StoreFilePath(folderPath, filename, res); return res; } catch (Exception) { IOConnectionInfo res; if (!TryGetCachedFilePath(folderPath, filename, out res)) throw; return res; } } public bool IsPermanentLocation(IOConnectionInfo ioc) { //even though the cache would be permanent, it's not a good idea to cache a temporary file, so return false in that case: return _cachedStorage.IsPermanentLocation(ioc); } public bool IsReadOnly(IOConnectionInfo ioc, OptionalOut reason = null) { //even though the cache can always be written, the changes made in the cache could not be transferred to the cached file //so we better treat the cache as read-only as well. return _cachedStorage.IsReadOnly(ioc, reason); } private void StoreFilePath(IOConnectionInfo folderPath, string filename, IOConnectionInfo res) { File.WriteAllText(CachedFilePath(GetPseudoIoc(folderPath, filename)) + ".filepath", res.Path); } private IOConnectionInfo GetPseudoIoc(IOConnectionInfo folderPath, string filename) { IOConnectionInfo res = folderPath.CloneDeep(); if (!res.Path.EndsWith("/")) res.Path += "/"; res.Path += filename; return res; } private bool TryGetCachedFilePath(IOConnectionInfo folderPath, string filename, out IOConnectionInfo res) { res = folderPath.CloneDeep(); string filePathCache = CachedFilePath(GetPseudoIoc(folderPath, filename)) + ".filepath"; if (!File.Exists(filePathCache)) return false; res.Path = File.ReadAllText(filePathCache); return true; } public string GetBaseVersionHash(IOConnectionInfo ioc) { return File.ReadAllText(BaseVersionFilePath(ioc)); } public string GetLocalVersionHash(IOConnectionInfo ioc) { return File.ReadAllText(VersionFilePath(ioc)); } public bool HasLocalChanges(IOConnectionInfo ioc) { return IsCached(ioc) && GetLocalVersionHash(ioc) != GetBaseVersionHash(ioc); } public Stream OpenRemoteForReadIfAvailable(IOConnectionInfo ioc) { try { return _cachedStorage.OpenFileForRead(ioc); } catch (Exception) { return File.OpenRead(CachedFilePath(ioc)); } } public bool IsOffline { get { return _cachedStorage.IsOffline; } set { _cachedStorage.IsOffline = value; } } public void OnRequestPermissionsResult(IFileStorageSetupActivity fileStorageSetupActivity, int requestCode, string[] permissions, Permission[] grantResults) { _cachedStorage.OnRequestPermissionsResult(fileStorageSetupActivity, requestCode, permissions, grantResults); } } }