using Bit.Core.Abstractions; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Response; using Bit.Core.Utilities; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Bit.Core.Services { public class SyncService : ISyncService { private const string Keys_LastSyncFormat = "lastSync_{0}"; private readonly IUserService _userService; private readonly IApiService _apiService; private readonly ISettingsService _settingsService; private readonly IFolderService _folderService; private readonly ICipherService _cipherService; private readonly ICryptoService _cryptoService; private readonly ICollectionService _collectionService; private readonly IStorageService _storageService; private readonly IMessagingService _messagingService; private readonly IPolicyService _policyService; private readonly Func _logoutCallbackAsync; public SyncService( IUserService userService, IApiService apiService, ISettingsService settingsService, IFolderService folderService, ICipherService cipherService, ICryptoService cryptoService, ICollectionService collectionService, IStorageService storageService, IMessagingService messagingService, IPolicyService policyService, Func logoutCallbackAsync) { _userService = userService; _apiService = apiService; _settingsService = settingsService; _folderService = folderService; _cipherService = cipherService; _cryptoService = cryptoService; _collectionService = collectionService; _storageService = storageService; _messagingService = messagingService; _policyService = policyService; _logoutCallbackAsync = logoutCallbackAsync; } public bool SyncInProgress { get; set; } public async Task GetLastSyncAsync() { var userId = await _userService.GetUserIdAsync(); if(userId == null) { return null; } return await _storageService.GetAsync(string.Format(Keys_LastSyncFormat, userId)); } public async Task SetLastSyncAsync(DateTime date) { var userId = await _userService.GetUserIdAsync(); if(userId == null) { return; } await _storageService.SaveAsync(string.Format(Keys_LastSyncFormat, userId), date); } public async Task FullSyncAsync(bool forceSync, bool allowThrowOnError = false) { SyncStarted(); var isAuthenticated = await _userService.IsAuthenticatedAsync(); if(!isAuthenticated) { return SyncCompleted(false); } var now = DateTime.UtcNow; var needsSyncResult = await NeedsSyncingAsync(forceSync); var needsSync = needsSyncResult.Item1; var skipped = needsSyncResult.Item2; if(skipped) { return SyncCompleted(false); } if(!needsSync) { await SetLastSyncAsync(now); return SyncCompleted(false); } var userId = await _userService.GetUserIdAsync(); try { var response = await _apiService.GetSyncAsync(); await SyncProfileAsync(response.Profile); await SyncFoldersAsync(userId, response.Folders); await SyncCollectionsAsync(response.Collections); await SyncCiphersAsync(userId, response.Ciphers); await SyncSettingsAsync(userId, response.Domains); await SyncPolicies(response.Policies); await SetLastSyncAsync(now); return SyncCompleted(true); } catch { if(allowThrowOnError) { throw; } else { return SyncCompleted(false); } } } public async Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit) { SyncStarted(); if(await _userService.IsAuthenticatedAsync()) { try { var localFolder = await _folderService.GetAsync(notification.Id); if((!isEdit && localFolder == null) || (isEdit && localFolder != null && localFolder.RevisionDate < notification.RevisionDate)) { var remoteFolder = await _apiService.GetFolderAsync(notification.Id); if(remoteFolder != null) { var userId = await _userService.GetUserIdAsync(); await _folderService.UpsertAsync(new FolderData(remoteFolder, userId)); _messagingService.Send("syncedUpsertedFolder", new Dictionary { ["folderId"] = notification.Id }); return SyncCompleted(true); } } } catch { } } return SyncCompleted(false); } public async Task SyncDeleteFolderAsync(SyncFolderNotification notification) { SyncStarted(); if(await _userService.IsAuthenticatedAsync()) { await _folderService.DeleteAsync(notification.Id); _messagingService.Send("syncedDeletedFolder", new Dictionary { ["folderId"] = notification.Id }); return SyncCompleted(true); } return SyncCompleted(false); } public async Task SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit) { SyncStarted(); if(await _userService.IsAuthenticatedAsync()) { try { var shouldUpdate = true; var localCipher = await _cipherService.GetAsync(notification.Id); if(localCipher != null && localCipher.RevisionDate >= notification.RevisionDate) { shouldUpdate = false; } var checkCollections = false; if(shouldUpdate) { if(isEdit) { shouldUpdate = localCipher != null; checkCollections = true; } else { if(notification.CollectionIds == null || notification.OrganizationId == null) { shouldUpdate = localCipher == null; } else { shouldUpdate = false; checkCollections = true; } } } if(!shouldUpdate && checkCollections && notification.OrganizationId != null && notification.CollectionIds != null && notification.CollectionIds.Any()) { var collections = await _collectionService.GetAllAsync(); if(collections != null) { foreach(var c in collections) { if(notification.CollectionIds.Contains(c.Id)) { shouldUpdate = true; break; } } } } if(shouldUpdate) { var remoteCipher = await _apiService.GetCipherAsync(notification.Id); if(remoteCipher != null) { var userId = await _userService.GetUserIdAsync(); await _cipherService.UpsertAsync(new CipherData(remoteCipher, userId)); _messagingService.Send("syncedUpsertedCipher", new Dictionary { ["cipherId"] = notification.Id }); return SyncCompleted(true); } } } catch(ApiException e) { if(e.Error != null && e.Error.StatusCode == System.Net.HttpStatusCode.NotFound && isEdit) { await _cipherService.DeleteAsync(notification.Id); _messagingService.Send("syncedDeletedCipher", new Dictionary { ["cipherId"] = notification.Id }); return SyncCompleted(true); } } } return SyncCompleted(false); } public async Task SyncDeleteCipherAsync(SyncCipherNotification notification) { SyncStarted(); if(await _userService.IsAuthenticatedAsync()) { await _cipherService.DeleteAsync(notification.Id); _messagingService.Send("syncedDeletedCipher", new Dictionary { ["cipherId"] = notification.Id }); return SyncCompleted(true); } return SyncCompleted(false); } // Helpers private void SyncStarted() { SyncInProgress = true; _messagingService.Send("syncStarted"); } private bool SyncCompleted(bool successfully) { SyncInProgress = false; _messagingService.Send("syncCompleted", new Dictionary { ["successfully"] = successfully }); return successfully; } private async Task> NeedsSyncingAsync(bool forceSync) { if(forceSync) { return new Tuple(true, false); } var lastSync = await GetLastSyncAsync(); if(lastSync == null || lastSync == DateTime.MinValue) { return new Tuple(true, false); } try { var response = await _apiService.GetAccountRevisionDateAsync(); var d = CoreHelpers.Epoc.AddMilliseconds(response); if(d <= lastSync.Value) { return new Tuple(false, false); } return new Tuple(true, false); } catch { return new Tuple(false, true); } } private async Task SyncProfileAsync(ProfileResponse response) { var stamp = await _userService.GetSecurityStampAsync(); if(stamp != null && stamp != response.SecurityStamp) { if(_logoutCallbackAsync != null) { await _logoutCallbackAsync(true); } return; } await _cryptoService.SetEncKeyAsync(response.Key); await _cryptoService.SetEncPrivateKeyAsync(response.PrivateKey); await _cryptoService.SetOrgKeysAsync(response.Organizations); await _userService.SetSecurityStampAsync(response.SecurityStamp); var organizations = response.Organizations.ToDictionary(o => o.Id, o => new OrganizationData(o)); await _userService.ReplaceOrganizationsAsync(organizations); } private async Task SyncFoldersAsync(string userId, List response) { var folders = response.ToDictionary(f => f.Id, f => new FolderData(f, userId)); await _folderService.ReplaceAsync(folders); } private async Task SyncCollectionsAsync(List response) { var collections = response.ToDictionary(c => c.Id, c => new CollectionData(c)); await _collectionService.ReplaceAsync(collections); } private async Task SyncCiphersAsync(string userId, List response) { var ciphers = response.ToDictionary(c => c.Id, c => new CipherData(c, userId)); await _cipherService.ReplaceAsync(ciphers); } private async Task SyncSettingsAsync(string userId, DomainsResponse response) { var eqDomains = new List>(); if(response != null && response.EquivalentDomains != null) { eqDomains = eqDomains.Concat(response.EquivalentDomains).ToList(); } if(response != null && response.GlobalEquivalentDomains != null) { foreach(var global in response.GlobalEquivalentDomains) { if(global.Domains.Any()) { eqDomains.Add(global.Domains); } } } await _settingsService.SetEquivalentDomainsAsync(eqDomains); } private async Task SyncPolicies(List response) { var policies = response?.ToDictionary(p => p.Id, p => new PolicyData(p)) ?? new Dictionary(); await _policyService.Replace(policies); } } }