diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index ee6bb23df..6c5a54729 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -129,6 +129,7 @@ namespace Bit.Android .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) // Other .RegisterInstance(CrossDeviceInfo.Current, new ContainerControlledLifetimeManager()) .RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager()) diff --git a/src/App/Abstractions/Repositories/ICipherApiRepository.cs b/src/App/Abstractions/Repositories/ICipherApiRepository.cs new file mode 100644 index 000000000..a25dae4bd --- /dev/null +++ b/src/App/Abstractions/Repositories/ICipherApiRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Models.Api; + +namespace Bit.App.Abstractions +{ + public interface ICipherApiRepository + { + Task> GetByIdAsync(string id); + Task>> GetAsync(); + Task> GetByRevisionDateWithHistoryAsync(DateTime since); + } +} \ No newline at end of file diff --git a/src/App/Abstractions/Repositories/IFolderApiRepository.cs b/src/App/Abstractions/Repositories/IFolderApiRepository.cs index c46e84aa4..74bb57985 100644 --- a/src/App/Abstractions/Repositories/IFolderApiRepository.cs +++ b/src/App/Abstractions/Repositories/IFolderApiRepository.cs @@ -6,7 +6,5 @@ using Bit.App.Models.Api; namespace Bit.App.Abstractions { public interface IFolderApiRepository : IApiRepository - { - Task>> GetByRevisionDateAsync(DateTime since); - } + { } } \ No newline at end of file diff --git a/src/App/Abstractions/Repositories/ISiteApiRepository.cs b/src/App/Abstractions/Repositories/ISiteApiRepository.cs index 6cd45b70d..eb13beb6a 100644 --- a/src/App/Abstractions/Repositories/ISiteApiRepository.cs +++ b/src/App/Abstractions/Repositories/ISiteApiRepository.cs @@ -7,6 +7,5 @@ namespace Bit.App.Abstractions { public interface ISiteApiRepository : IApiRepository { - Task>> GetByRevisionDateAsync(DateTime since); } } \ No newline at end of file diff --git a/src/App/Abstractions/Services/ISyncService.cs b/src/App/Abstractions/Services/ISyncService.cs index 4317e67da..b77107ddc 100644 --- a/src/App/Abstractions/Services/ISyncService.cs +++ b/src/App/Abstractions/Services/ISyncService.cs @@ -4,6 +4,10 @@ namespace Bit.App.Abstractions { public interface ISyncService { - Task SyncAsync(); + Task SyncAsync(string id); + Task SyncDeleteFolderAsync(string id); + Task SyncDeleteSiteAsync(string id); + Task FullSyncAsync(); + Task IncrementalSyncAsync(); } -} \ No newline at end of file +} diff --git a/src/App/App.csproj b/src/App/App.csproj index 32e81f426..fee663757 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -67,9 +67,12 @@ + + + @@ -77,6 +80,8 @@ + + @@ -84,6 +89,7 @@ + @@ -93,6 +99,7 @@ + @@ -116,6 +123,8 @@ + + diff --git a/src/App/Enums/CipherType.cs b/src/App/Enums/CipherType.cs new file mode 100644 index 000000000..5d364571b --- /dev/null +++ b/src/App/Enums/CipherType.cs @@ -0,0 +1,8 @@ +namespace Bit.App.Enums +{ + public enum CipherType : short + { + Folder = 0, + Site = 1 + } +} diff --git a/src/App/Enums/PushType.cs b/src/App/Enums/PushType.cs new file mode 100644 index 000000000..3d8586787 --- /dev/null +++ b/src/App/Enums/PushType.cs @@ -0,0 +1,11 @@ +namespace Bit.App.Enums +{ + public enum PushType : short + { + SyncCipherUpdate = 0, + SyncCipherCreate = 1, + SyncSiteDelete = 2, + SyncFolderDelete = 3, + SyncCiphers = 4 + } +} diff --git a/src/App/Models/Api/FolderDataModel.cs b/src/App/Models/Api/FolderDataModel.cs new file mode 100644 index 000000000..a5d7d7b6e --- /dev/null +++ b/src/App/Models/Api/FolderDataModel.cs @@ -0,0 +1,7 @@ +namespace Bit.App.Models.Api +{ + public class FolderDataModel + { + public string Name { get; set; } + } +} diff --git a/src/App/Models/Api/Response/CipherHistoryResponse.cs b/src/App/Models/Api/Response/CipherHistoryResponse.cs new file mode 100644 index 000000000..ef7bd4aa0 --- /dev/null +++ b/src/App/Models/Api/Response/CipherHistoryResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Bit.App.Models.Api +{ + public class CipherHistoryResponse + { + public IEnumerable Revised { get; set; } + public IEnumerable Deleted { get; set; } + } +} diff --git a/src/App/Models/Api/Response/CipherResponse.cs b/src/App/Models/Api/Response/CipherResponse.cs new file mode 100644 index 000000000..d22e481e1 --- /dev/null +++ b/src/App/Models/Api/Response/CipherResponse.cs @@ -0,0 +1,15 @@ +using Bit.App.Enums; +using System; + +namespace Bit.App.Models.Api +{ + public class CipherResponse + { + public string Id { get; set; } + public string FolderId { get; set; } + public CipherType Type { get; set; } + public bool Favorite { get; set; } + public dynamic Data { get; set; } + public DateTime RevisionDate { get; set; } + } +} diff --git a/src/App/Models/Api/SiteDataModel.cs b/src/App/Models/Api/SiteDataModel.cs new file mode 100644 index 000000000..a86da09a0 --- /dev/null +++ b/src/App/Models/Api/SiteDataModel.cs @@ -0,0 +1,11 @@ +namespace Bit.App.Models.Api +{ + public class SiteDataModel + { + public string Name { get; set; } + public string Uri { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string Notes { get; set; } + } +} diff --git a/src/App/Models/Data/FolderData.cs b/src/App/Models/Data/FolderData.cs index fd38e36c2..9eaf06b10 100644 --- a/src/App/Models/Data/FolderData.cs +++ b/src/App/Models/Data/FolderData.cs @@ -25,6 +25,24 @@ namespace Bit.App.Models.Data Name = folder.Name; } + public FolderData(CipherResponse cipher, string userId) + { + if(cipher.Type != Enums.CipherType.Folder) + { + throw new ArgumentException(nameof(cipher.Type)); + } + + var data = cipher.Data as FolderDataModel; + if(data == null) + { + throw new ArgumentException(nameof(cipher.Data)); + } + + Id = cipher.Id; + UserId = userId; + Name = data.Name; + } + [PrimaryKey] public string Id { get; set; } [Indexed] diff --git a/src/App/Models/Data/SiteData.cs b/src/App/Models/Data/SiteData.cs index 9a1921056..48620f7db 100644 --- a/src/App/Models/Data/SiteData.cs +++ b/src/App/Models/Data/SiteData.cs @@ -37,6 +37,29 @@ namespace Bit.App.Models.Data Favorite = site.Favorite; } + public SiteData(CipherResponse cipher, string userId) + { + if(cipher.Type != Enums.CipherType.Site) + { + throw new ArgumentException(nameof(cipher.Type)); + } + + var data = cipher.Data as SiteDataModel; + if(data == null) + { + throw new ArgumentException(nameof(cipher.Data)); + } + + Id = cipher.Id; + UserId = userId; + Name = data.Name; + Uri = data.Uri; + Username = data.Username; + Password = data.Password; + Notes = data.Notes; + Favorite = cipher.Favorite; + } + [PrimaryKey] public string Id { get; set; } public string FolderId { get; set; } diff --git a/src/App/Models/Page/VaultListPageModel.cs b/src/App/Models/Page/VaultListPageModel.cs index 4d3fd73d8..3576aaed8 100644 --- a/src/App/Models/Page/VaultListPageModel.cs +++ b/src/App/Models/Page/VaultListPageModel.cs @@ -15,7 +15,7 @@ namespace Bit.App.Models.Page Id = site.Id; FolderId = folderId; Name = site.Name?.Decrypt(); - Username = site.Username?.Decrypt() ?? " "; + Username = site.Username?.Decrypt(); Password = site.Password?.Decrypt(); Uri = site.Uri?.Decrypt(); } diff --git a/src/App/Models/PushNotification.cs b/src/App/Models/PushNotification.cs new file mode 100644 index 000000000..2fe72e27e --- /dev/null +++ b/src/App/Models/PushNotification.cs @@ -0,0 +1,19 @@ +using Bit.App.Enums; + +namespace Bit.App.Models +{ + public class PushNotification + { + public PushType Type { get; set; } + } + + public class SyncPushNotification : PushNotification + { + public string UserId { get; set; } + } + + public class SyncCipherPushNotification : SyncPushNotification + { + public string CipherId { get; set; } + } +} diff --git a/src/App/Repositories/CipherApiRepository.cs b/src/App/Repositories/CipherApiRepository.cs new file mode 100644 index 000000000..c44199107 --- /dev/null +++ b/src/App/Repositories/CipherApiRepository.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Api; +using Newtonsoft.Json; + +namespace Bit.App.Repositories +{ + public class CipherApiRepository : BaseApiRepository, ICipherApiRepository + { + protected override string ApiRoute => "ciphers"; + + public virtual async Task> GetByIdAsync(string id) + { + var requestMessage = new TokenHttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/", id)), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseContent); + return ApiResult.Success(responseObj, response.StatusCode); + } + + public virtual async Task>> GetAsync() + { + var requestMessage = new TokenHttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = new Uri(Client.BaseAddress, ApiRoute), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject>(responseContent); + return ApiResult>.Success(responseObj, response.StatusCode); + } + + public virtual async Task> GetByRevisionDateWithHistoryAsync(DateTime since) + { + var requestMessage = new TokenHttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "?since=", since)), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseContent); + return ApiResult.Success(responseObj, response.StatusCode); + } + } +} diff --git a/src/App/Repositories/SiteApiRepository.cs b/src/App/Repositories/SiteApiRepository.cs index 67f746b65..a90ca3e6d 100644 --- a/src/App/Repositories/SiteApiRepository.cs +++ b/src/App/Repositories/SiteApiRepository.cs @@ -11,24 +11,5 @@ namespace Bit.App.Repositories public class SiteApiRepository : ApiRepository, ISiteApiRepository { protected override string ApiRoute => "sites"; - - public virtual async Task>> GetByRevisionDateAsync(DateTime since) - { - var requestMessage = new TokenHttpRequestMessage() - { - Method = HttpMethod.Get, - RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "?since=", since)), - }; - - var response = await Client.SendAsync(requestMessage); - if(!response.IsSuccessStatusCode) - { - return await HandleErrorAsync>(response); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - var responseObj = JsonConvert.DeserializeObject>(responseContent); - return ApiResult>.Success(responseObj, response.StatusCode); - } } } diff --git a/src/App/Services/PushNotificationListener.cs b/src/App/Services/PushNotificationListener.cs index 529cb293b..79d7d6173 100644 --- a/src/App/Services/PushNotificationListener.cs +++ b/src/App/Services/PushNotificationListener.cs @@ -4,6 +4,7 @@ using PushNotification.Plugin; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Bit.App.Abstractions; +using Bit.App.Models; namespace Bit.App.Services { @@ -31,6 +32,29 @@ namespace Bit.App.Services { _showNotification = false; Debug.WriteLine("Message Arrived: {0}", JsonConvert.SerializeObject(values)); + + var type = (Enums.PushType)values.GetValue("type", System.StringComparison.OrdinalIgnoreCase).ToObject(); + switch(type) + { + case Enums.PushType.SyncCipherUpdate: + case Enums.PushType.SyncCipherCreate: + var createUpdateMessage = values.ToObject(); + _syncService.SyncAsync(createUpdateMessage.CipherId); + break; + case Enums.PushType.SyncFolderDelete: + var folderDeleteMessage = values.ToObject(); + _syncService.SyncDeleteFolderAsync(folderDeleteMessage.CipherId); + break; + case Enums.PushType.SyncSiteDelete: + var siteDeleteMessage = values.ToObject(); + _syncService.SyncDeleteFolderAsync(siteDeleteMessage.CipherId); + break; + case Enums.PushType.SyncCiphers: + _syncService.FullSyncAsync(); + break; + default: + break; + } } public async void OnRegistered(string token, DeviceType deviceType) diff --git a/src/App/Services/SyncService.cs b/src/App/Services/SyncService.cs index 0b614f168..e254dac60 100644 --- a/src/App/Services/SyncService.cs +++ b/src/App/Services/SyncService.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Models.Data; using Plugin.Settings.Abstractions; +using Bit.App.Models.Api; +using System.Collections.Generic; namespace Bit.App.Services { @@ -11,6 +13,7 @@ namespace Bit.App.Services { private const string LastSyncKey = "lastSync"; + private readonly ICipherApiRepository _cipherApiRepository; private readonly IFolderApiRepository _folderApiRepository; private readonly ISiteApiRepository _siteApiRepository; private readonly IFolderRepository _folderRepository; @@ -19,6 +22,7 @@ namespace Bit.App.Services private readonly ISettings _settings; public SyncService( + ICipherApiRepository cipherApiRepository, IFolderApiRepository folderApiRepository, ISiteApiRepository siteApiRepository, IFolderRepository folderRepository, @@ -26,6 +30,7 @@ namespace Bit.App.Services IAuthService authService, ISettings settings) { + _cipherApiRepository = cipherApiRepository; _folderApiRepository = folderApiRepository; _siteApiRepository = siteApiRepository; _folderRepository = folderRepository; @@ -34,36 +39,145 @@ namespace Bit.App.Services _settings = settings; } - public async Task SyncAsync() + public async Task SyncAsync(string id) { - var now = DateTime.UtcNow; - var lastSync = _settings.GetValueOrDefault(LastSyncKey, now.AddYears(-100)); - - var siteTask = SyncSitesAsync(lastSync); - var folderTask = SyncFoldersAsync(lastSync); - await Task.WhenAll(siteTask, folderTask); - - if(await siteTask && await folderTask && folderTask.Exception == null && siteTask.Exception == null) - { - _settings.AddOrUpdateValue(LastSyncKey, now); - return true; - } - - return false; - } - - private async Task SyncFoldersAsync(DateTime lastSync) - { - var folderResponse = await _folderApiRepository.GetAsync(); - if(!folderResponse.Succeeded) + if(!_authService.IsAuthenticated) { return false; } - var serverFolders = folderResponse.Result.Data; + var cipher = await _cipherApiRepository.GetByIdAsync(id); + if(!cipher.Succeeded) + { + return false; + } + + switch(cipher.Result.Type) + { + case Enums.CipherType.Folder: + var folderData = new FolderData(cipher.Result, _authService.UserId); + var existingLocalFolder = _folderRepository.GetByIdAsync(id); + if(existingLocalFolder == null) + { + await _folderRepository.InsertAsync(folderData); + } + else + { + await _folderRepository.UpdateAsync(folderData); + } + break; + case Enums.CipherType.Site: + var siteData = new SiteData(cipher.Result, _authService.UserId); + var existingLocalSite = _siteRepository.GetByIdAsync(id); + if(existingLocalSite == null) + { + await _siteRepository.InsertAsync(siteData); + } + else + { + await _siteRepository.UpdateAsync(siteData); + } + break; + default: + return false; + } + + return true; + } + + public async Task SyncDeleteFolderAsync(string id) + { + if(!_authService.IsAuthenticated) + { + return false; + } + + await _folderRepository.DeleteAsync(id); + return true; + } + + public async Task SyncDeleteSiteAsync(string id) + { + if(!_authService.IsAuthenticated) + { + return false; + } + + await _siteRepository.DeleteAsync(id); + return true; + } + + public async Task FullSyncAsync() + { + if(!_authService.IsAuthenticated) + { + return false; + } + + var now = DateTime.UtcNow; + var ciphers = await _cipherApiRepository.GetAsync(); + if(!ciphers.Succeeded) + { + return false; + } + + var siteTask = SyncSitesAsync(ciphers.Result.Data.Where(c => c.Type == Enums.CipherType.Site), true); + var folderTask = SyncFoldersAsync(ciphers.Result.Data.Where(c => c.Type == Enums.CipherType.Folder), true); + await Task.WhenAll(siteTask, folderTask); + + if(folderTask.Exception == null || siteTask.Exception == null) + { + return false; + } + + _settings.AddOrUpdateValue(LastSyncKey, now); + return true; + } + + public async Task IncrementalSyncAsync() + { + if(!_authService.IsAuthenticated) + { + return false; + } + + var now = DateTime.UtcNow; + DateTime? lastSync = _settings.GetValueOrDefault(LastSyncKey); + if(lastSync == null) + { + return await FullSyncAsync(); + } + + var ciphers = await _cipherApiRepository.GetByRevisionDateWithHistoryAsync(lastSync.Value); + if(!ciphers.Succeeded) + { + return false; + } + + var siteTask = SyncSitesAsync(ciphers.Result.Revised.Where(c => c.Type == Enums.CipherType.Site), false); + var folderTask = SyncFoldersAsync(ciphers.Result.Revised.Where(c => c.Type == Enums.CipherType.Folder), false); + + foreach(var cipherId in ciphers.Result.Deleted) + { + await _siteRepository.DeleteAsync(cipherId); + } + + await Task.WhenAll(siteTask, folderTask); + + if(folderTask.Exception == null || siteTask.Exception == null) + { + return false; + } + + _settings.AddOrUpdateValue(LastSyncKey, now); + return true; + } + + private async Task SyncFoldersAsync(IEnumerable serverFolders, bool deleteMissing) + { var localFolders = await _folderRepository.GetAllByUserIdAsync(_authService.UserId); - foreach(var serverFolder in serverFolders.Where(f => f.RevisionDate >= lastSync)) + foreach(var serverFolder in serverFolders) { var data = new FolderData(serverFolder, _authService.UserId); var existingLocalFolder = localFolders.SingleOrDefault(f => f.Id == serverFolder.Id); @@ -77,26 +191,22 @@ namespace Bit.App.Services } } + if(!deleteMissing) + { + return; + } + foreach(var folder in localFolders.Where(localFolder => !serverFolders.Any(serverFolder => serverFolder.Id == localFolder.Id))) { await _folderRepository.DeleteAsync(folder.Id); } - - return true; } - private async Task SyncSitesAsync(DateTime lastSync) + private async Task SyncSitesAsync(IEnumerable serverSites, bool deleteMissing) { - var siteResponse = await _siteApiRepository.GetAsync(); - if(!siteResponse.Succeeded) - { - return false; - } - - var serverSites = siteResponse.Result.Data; var localSites = await _siteRepository.GetAllByUserIdAsync(_authService.UserId); - foreach(var serverSite in serverSites.Where(s => s.RevisionDate >= lastSync)) + foreach(var serverSite in serverSites) { var data = new SiteData(serverSite, _authService.UserId); var existingLocalSite = localSites.SingleOrDefault(s => s.Id == serverSite.Id); @@ -110,12 +220,15 @@ namespace Bit.App.Services } } + if(!deleteMissing) + { + return; + } + foreach(var site in localSites.Where(localSite => !serverSites.Any(serverSite => serverSite.Id == localSite.Id))) { await _siteRepository.DeleteAsync(site.Id); } - - return true; } } } diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 2912c1a2d..5828a0003 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -189,6 +189,7 @@ namespace Bit.iOS .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) // Other .RegisterInstance(CrossDeviceInfo.Current, new ContainerControlledLifetimeManager()) .RegisterInstance(CrossConnectivity.Current, new ContainerControlledLifetimeManager())