using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bit.Core.Abstractions; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Response; using Bit.Core.Utilities; namespace Bit.Core.Services { public class SyncService : ISyncService { private readonly IStateService _stateService; 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 IOrganizationService _organizationService; private readonly IMessagingService _messagingService; private readonly IPolicyService _policyService; private readonly ISendService _sendService; private readonly IKeyConnectorService _keyConnectorService; private readonly ILogger _logger; private readonly Func, Task> _logoutCallbackAsync; private readonly LazyResolve _watchDeviceService = new LazyResolve(); public SyncService( IStateService stateService, IApiService apiService, ISettingsService settingsService, IFolderService folderService, ICipherService cipherService, ICryptoService cryptoService, ICollectionService collectionService, IOrganizationService organizationService, IMessagingService messagingService, IPolicyService policyService, ISendService sendService, IKeyConnectorService keyConnectorService, ILogger logger, Func, Task> logoutCallbackAsync) { _stateService = stateService; _apiService = apiService; _settingsService = settingsService; _folderService = folderService; _cipherService = cipherService; _cryptoService = cryptoService; _collectionService = collectionService; _organizationService = organizationService; _messagingService = messagingService; _policyService = policyService; _sendService = sendService; _keyConnectorService = keyConnectorService; _logger = logger; _logoutCallbackAsync = logoutCallbackAsync; } public bool SyncInProgress { get; set; } public async Task GetLastSyncAsync() { if (await _stateService.GetActiveUserIdAsync() == null) { return null; } return await _stateService.GetLastSyncAsync(); } public async Task SetLastSyncAsync(DateTime date) { if (await _stateService.GetActiveUserIdAsync() == null) { return; } await _stateService.SetLastSyncAsync(date); } public async Task FullSyncAsync(bool forceSync, bool allowThrowOnError = false) { SyncStarted(); var isAuthenticated = await _stateService.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 _stateService.GetActiveUserIdAsync(); 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 SyncPoliciesAsync(response.Policies); await SyncSendsAsync(userId, response.Sends); await SetLastSyncAsync(now); _watchDeviceService.Value.SyncDataToWatchAsync().FireAndForget(); return SyncCompleted(true); } catch { if (allowThrowOnError) { throw; } else { return SyncCompleted(false); } } } public async Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit) { SyncStarted(); if (await _stateService.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 _stateService.GetActiveUserIdAsync(); 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 _stateService.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 _stateService.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 _stateService.GetActiveUserIdAsync(); 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 _stateService.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 _stateService.GetSecurityStampAsync(); if (stamp != null && stamp != response.SecurityStamp) { if (_logoutCallbackAsync != null) { await _logoutCallbackAsync(new Tuple(response.Id, false, true)); } return; } await _cryptoService.SetEncKeyAsync(response.Key); await _cryptoService.SetEncPrivateKeyAsync(response.PrivateKey); await _cryptoService.SetOrgKeysAsync(response.Organizations); await _stateService.SetSecurityStampAsync(response.SecurityStamp); var organizations = response.Organizations.ToDictionary(o => o.Id, o => new OrganizationData(o)); await _organizationService.ReplaceAsync(organizations); await _stateService.SetEmailVerifiedAsync(response.EmailVerified); await _stateService.SetNameAsync(response.Name); await _stateService.SetAvatarColorAsync(response.AvatarColor); await _keyConnectorService.SetUsesKeyConnector(response.UsesKeyConnector); } 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 SyncPoliciesAsync(List response) { var policies = response?.ToDictionary(p => p.Id, p => new PolicyData(p)) ?? new Dictionary(); await _policyService.Replace(policies); } private async Task SyncSendsAsync(string userId, List response) { var sends = response?.ToDictionary(s => s.Id, s => new SendData(s, userId)) ?? new Dictionary(); await _sendService.ReplaceAsync(sends); } public async Task SyncPasswordlessLoginRequestsAsync() { try { var userId = await _stateService.GetActiveUserIdAsync(); // if the user has not enabled passwordless logins ignore requests if (!await _stateService.GetApprovePasswordlessLoginsAsync(userId)) { return; } var loginRequests = await _apiService.GetAuthRequestAsync(); if (loginRequests == null || !loginRequests.Any()) { return; } var validLoginRequest = loginRequests.Where(l => !l.IsAnswered && !l.IsExpired) .OrderByDescending(x => x.CreationDate) .FirstOrDefault(); if (validLoginRequest is null) { return; } await _stateService.SetPasswordlessLoginNotificationAsync(new PasswordlessRequestNotification() { Id = validLoginRequest.Id, UserId = userId }); _messagingService.Send(Constants.PasswordlessLoginRequestKey); } catch (Exception ex) { _logger.Exception(ex); } } } }