bitwarden-mobile/src/Core/Services/SyncService.cs

435 lines
17 KiB
C#

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<Tuple<string, bool, bool>, Task> _logoutCallbackAsync;
private readonly LazyResolve<IWatchDeviceService> _watchDeviceService = new LazyResolve<IWatchDeviceService>();
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<Tuple<string, bool, bool>, 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<DateTime?> 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<bool> 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<bool> 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<string, string>
{
["folderId"] = notification.Id
});
return SyncCompleted(true);
}
}
}
catch { }
}
return SyncCompleted(false);
}
public async Task<bool> SyncDeleteFolderAsync(SyncFolderNotification notification)
{
SyncStarted();
if (await _stateService.IsAuthenticatedAsync())
{
await _folderService.DeleteAsync(notification.Id);
_messagingService.Send("syncedDeletedFolder", new Dictionary<string, string>
{
["folderId"] = notification.Id
});
return SyncCompleted(true);
}
return SyncCompleted(false);
}
public async Task<bool> 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<string, string>
{
["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<string, string>
{
["cipherId"] = notification.Id
});
return SyncCompleted(true);
}
}
}
return SyncCompleted(false);
}
public async Task<bool> SyncDeleteCipherAsync(SyncCipherNotification notification)
{
SyncStarted();
if (await _stateService.IsAuthenticatedAsync())
{
await _cipherService.DeleteAsync(notification.Id);
_messagingService.Send("syncedDeletedCipher", new Dictionary<string, string>
{
["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<string, object> { ["successfully"] = successfully });
return successfully;
}
private async Task<Tuple<bool, bool>> NeedsSyncingAsync(bool forceSync)
{
if (forceSync)
{
return new Tuple<bool, bool>(true, false);
}
var lastSync = await GetLastSyncAsync();
if (lastSync == null || lastSync == DateTime.MinValue)
{
return new Tuple<bool, bool>(true, false);
}
try
{
var response = await _apiService.GetAccountRevisionDateAsync();
var d = CoreHelpers.Epoc.AddMilliseconds(response);
if (d <= lastSync.Value)
{
return new Tuple<bool, bool>(false, false);
}
return new Tuple<bool, bool>(true, false);
}
catch
{
return new Tuple<bool, bool>(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<string, bool, bool>(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<FolderResponse> response)
{
var folders = response.ToDictionary(f => f.Id, f => new FolderData(f, userId));
await _folderService.ReplaceAsync(folders);
}
private async Task SyncCollectionsAsync(List<CollectionDetailsResponse> response)
{
var collections = response.ToDictionary(c => c.Id, c => new CollectionData(c));
await _collectionService.ReplaceAsync(collections);
}
private async Task SyncCiphersAsync(string userId, List<CipherResponse> 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<List<string>>();
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<PolicyResponse> response)
{
var policies = response?.ToDictionary(p => p.Id, p => new PolicyData(p)) ??
new Dictionary<string, PolicyData>();
await _policyService.Replace(policies);
}
private async Task SyncSendsAsync(string userId, List<SendResponse> response)
{
var sends = response?.ToDictionary(s => s.Id, s => new SendData(s, userId)) ??
new Dictionary<string, SendData>();
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);
}
}
}
}