mirror of
https://github.com/bitwarden/android.git
synced 2025-01-11 18:57:39 +03:00
more cipher service functions
This commit is contained in:
parent
25c82ffd58
commit
40b6460ac9
1 changed files with 324 additions and 7 deletions
|
@ -1,12 +1,17 @@
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Models.Request;
|
||||||
|
using Bit.Core.Models.Response;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
@ -26,11 +31,12 @@ namespace Bit.Core.Services
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IApiService _apiService;
|
private readonly IApiService _apiService;
|
||||||
private readonly IStorageService _storageService;
|
private readonly IStorageService _storageService;
|
||||||
private readonly II18nService _i18NService;
|
private readonly II18nService _i18nService;
|
||||||
private Dictionary<string, HashSet<string>> _domainMatchBlacklist = new Dictionary<string, HashSet<string>>
|
private Dictionary<string, HashSet<string>> _domainMatchBlacklist = new Dictionary<string, HashSet<string>>
|
||||||
{
|
{
|
||||||
["google.com"] = new HashSet<string> { "script.google.com" }
|
["google.com"] = new HashSet<string> { "script.google.com" }
|
||||||
};
|
};
|
||||||
|
private readonly HttpClient _httpClient = new HttpClient();
|
||||||
|
|
||||||
public CipherService(
|
public CipherService(
|
||||||
ICryptoService cryptoService,
|
ICryptoService cryptoService,
|
||||||
|
@ -45,7 +51,7 @@ namespace Bit.Core.Services
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_apiService = apiService;
|
_apiService = apiService;
|
||||||
_storageService = storageService;
|
_storageService = storageService;
|
||||||
_i18NService = i18nService;
|
_i18nService = i18nService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<CipherView> DecryptedCipherCache
|
private List<CipherView> DecryptedCipherCache
|
||||||
|
@ -67,7 +73,7 @@ namespace Bit.Core.Services
|
||||||
DecryptedCipherCache = null;
|
DecryptedCipherCache = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Cipher> Encrypt(CipherView model, SymmetricCryptoKey key = null,
|
public async Task<Cipher> EncryptAsync(CipherView model, SymmetricCryptoKey key = null,
|
||||||
Cipher originalCipher = null)
|
Cipher originalCipher = null)
|
||||||
{
|
{
|
||||||
// Adjust password history
|
// Adjust password history
|
||||||
|
@ -370,8 +376,225 @@ namespace Bit.Core.Services
|
||||||
matchingLogins, matchingFuzzyLogins, others);
|
matchingLogins, matchingFuzzyLogins, others);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<CipherView> GetLastUsedForUrlAsync(string url)
|
||||||
|
{
|
||||||
|
var ciphers = await GetAllDecryptedForUrlAsync(url);
|
||||||
|
return ciphers.OrderBy(c => c, new CipherLastUsedComparer()).FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLastUsedDateAsync(string id)
|
||||||
|
{
|
||||||
|
var ciphersLocalData = await _storageService.GetAsync<Dictionary<string, Dictionary<string, object>>>(
|
||||||
|
Keys_LocalData);
|
||||||
|
if(ciphersLocalData == null)
|
||||||
|
{
|
||||||
|
ciphersLocalData = new Dictionary<string, Dictionary<string, object>>();
|
||||||
|
}
|
||||||
|
if(!ciphersLocalData.ContainsKey(id))
|
||||||
|
{
|
||||||
|
ciphersLocalData.Add(id, new Dictionary<string, object>());
|
||||||
|
}
|
||||||
|
if(ciphersLocalData[id].ContainsKey("lastUsedDate"))
|
||||||
|
{
|
||||||
|
ciphersLocalData[id]["lastUsedDate"] = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ciphersLocalData[id].Add("lastUsedDate", DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _storageService.SaveAsync(Keys_LocalData, ciphersLocalData);
|
||||||
|
// Update cache
|
||||||
|
if(DecryptedCipherCache == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cached = DecryptedCipherCache.FirstOrDefault(c => c.Id == id);
|
||||||
|
if(cached != null)
|
||||||
|
{
|
||||||
|
cached.LocalData = ciphersLocalData[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveNeverDomainAsync(string domain)
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(domain))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var domains = await _storageService.GetAsync<HashSet<string>>(Keys_NeverDomains);
|
||||||
|
if(domains == null)
|
||||||
|
{
|
||||||
|
domains = new HashSet<string>();
|
||||||
|
}
|
||||||
|
domains.Add(domain);
|
||||||
|
await _storageService.SaveAsync(Keys_NeverDomains, domains);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveWithServerAsync(Cipher cipher)
|
||||||
|
{
|
||||||
|
CipherResponse response;
|
||||||
|
if(cipher.Id == null)
|
||||||
|
{
|
||||||
|
if(cipher.CollectionIds != null)
|
||||||
|
{
|
||||||
|
var request = new CipherCreateRequest(cipher);
|
||||||
|
response = await _apiService.PostCipherCreateAsync(request);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var request = new CipherRequest(cipher);
|
||||||
|
response = await _apiService.PostCipherAsync(request);
|
||||||
|
}
|
||||||
|
cipher.Id = response.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var request = new CipherRequest(cipher);
|
||||||
|
response = await _apiService.PutCipherAsync(cipher.Id, request);
|
||||||
|
}
|
||||||
|
var userId = await _userService.GetUserIdAsync();
|
||||||
|
var data = new CipherData(response, userId, cipher.CollectionIds);
|
||||||
|
await UpsertAsync(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ShareWithServerAsync(CipherView cipher, string organizationId, HashSet<string> collectionIds)
|
||||||
|
{
|
||||||
|
var attachmentTasks = new List<Task>();
|
||||||
|
if(cipher.Attachments != null)
|
||||||
|
{
|
||||||
|
foreach(var attachment in cipher.Attachments)
|
||||||
|
{
|
||||||
|
if(attachment.Key == null)
|
||||||
|
{
|
||||||
|
attachmentTasks.Add(ShareAttachmentWithServerAsync(attachment, cipher.Id, organizationId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Task.WhenAll(attachmentTasks);
|
||||||
|
cipher.OrganizationId = organizationId;
|
||||||
|
cipher.CollectionIds = collectionIds;
|
||||||
|
var encCipher = await EncryptAsync(cipher);
|
||||||
|
var request = new CipherShareRequest(encCipher);
|
||||||
|
var response = await _apiService.PutShareCipherAsync(cipher.Id, request);
|
||||||
|
var userId = await _userService.GetUserIdAsync();
|
||||||
|
var data = new CipherData(response, userId, collectionIds);
|
||||||
|
await UpsertAsync(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Cipher> SaveAttachmentRawWithServerAsync(Cipher cipher, string filename, byte[] data)
|
||||||
|
{
|
||||||
|
var key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId);
|
||||||
|
var encFileName = await _cryptoService.EncryptAsync(filename, key);
|
||||||
|
var dataEncKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||||
|
var encData = await _cryptoService.EncryptToBytesAsync(data, dataEncKey.Item1);
|
||||||
|
|
||||||
|
CipherResponse response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using(var fd = new MultipartFormDataContent(string.Concat("Upload----", DateTime.UtcNow)))
|
||||||
|
{
|
||||||
|
fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString);
|
||||||
|
fd.Add(new StringContent(string.Empty), "key", dataEncKey.Item2.EncryptedString);
|
||||||
|
response = await _apiService.PostCipherAttachmentAsync(cipher.Id, fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ApiException e)
|
||||||
|
{
|
||||||
|
throw new Exception(e.Error.GetSingleMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = await _userService.GetUserIdAsync();
|
||||||
|
var cData = new CipherData(response, userId, cipher.CollectionIds);
|
||||||
|
await UpsertAsync(cData);
|
||||||
|
return new Cipher(cData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(CipherData cipher)
|
||||||
|
{
|
||||||
|
var userId = await _userService.GetUserIdAsync();
|
||||||
|
var storageKey = string.Format(Keys_CiphersFormat, userId);
|
||||||
|
var ciphers = await _storageService.GetAsync<Dictionary<string, CipherData>>(storageKey);
|
||||||
|
if(ciphers == null)
|
||||||
|
{
|
||||||
|
ciphers = new Dictionary<string, CipherData>();
|
||||||
|
}
|
||||||
|
if(!ciphers.ContainsKey(cipher.Id))
|
||||||
|
{
|
||||||
|
ciphers.Add(cipher.Id, null);
|
||||||
|
}
|
||||||
|
ciphers[cipher.Id] = cipher;
|
||||||
|
await _storageService.SaveAsync(storageKey, ciphers);
|
||||||
|
DecryptedCipherCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(List<CipherData> cipher)
|
||||||
|
{
|
||||||
|
var userId = await _userService.GetUserIdAsync();
|
||||||
|
var storageKey = string.Format(Keys_CiphersFormat, userId);
|
||||||
|
var ciphers = await _storageService.GetAsync<Dictionary<string, CipherData>>(storageKey);
|
||||||
|
if(ciphers == null)
|
||||||
|
{
|
||||||
|
ciphers = new Dictionary<string, CipherData>();
|
||||||
|
}
|
||||||
|
foreach(var c in cipher)
|
||||||
|
{
|
||||||
|
if(!ciphers.ContainsKey(c.Id))
|
||||||
|
{
|
||||||
|
ciphers.Add(c.Id, null);
|
||||||
|
}
|
||||||
|
ciphers[c.Id] = c;
|
||||||
|
}
|
||||||
|
await _storageService.SaveAsync(storageKey, ciphers);
|
||||||
|
DecryptedCipherCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RelaceAsync(Dictionary<string, CipherData> ciphers)
|
||||||
|
{
|
||||||
|
var userId = await _userService.GetUserIdAsync();
|
||||||
|
await _storageService.SaveAsync(string.Format(Keys_CiphersFormat, userId), ciphers);
|
||||||
|
DecryptedCipherCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearAsync(string userId)
|
||||||
|
{
|
||||||
|
await _storageService.RemoveAsync(string.Format(Keys_CiphersFormat, userId));
|
||||||
|
ClearCache();
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
|
private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId,
|
||||||
|
string organizationId)
|
||||||
|
{
|
||||||
|
var attachmentResponse = await _httpClient.GetAsync(attachmentView.Url);
|
||||||
|
if(!attachmentResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to download attachment: " + attachmentResponse.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = await attachmentResponse.Content.ReadAsByteArrayAsync();
|
||||||
|
var decBytes = await _cryptoService.DecryptFromBytesAsync(bytes, null);
|
||||||
|
var key = await _cryptoService.GetOrgKeyAsync(organizationId);
|
||||||
|
var encFileName = await _cryptoService.EncryptAsync(attachmentView.FileName, key);
|
||||||
|
var dataEncKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||||
|
var encData = await _cryptoService.EncryptToBytesAsync(decBytes, dataEncKey.Item1);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using(var fd = new MultipartFormDataContent(string.Concat("Upload----", DateTime.UtcNow)))
|
||||||
|
{
|
||||||
|
fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString);
|
||||||
|
fd.Add(new StringContent(string.Empty), "key", dataEncKey.Item2.EncryptedString);
|
||||||
|
await _apiService.PostShareCipherAttachmentAsync(cipherId, attachmentView.Id, fd, organizationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ApiException e)
|
||||||
|
{
|
||||||
|
throw new Exception(e.Error.GetSingleMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri,
|
private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri,
|
||||||
List<CipherView> matchingLogins, List<CipherView> matchingFuzzyLogins,
|
List<CipherView> matchingLogins, List<CipherView> matchingFuzzyLogins,
|
||||||
HashSet<string> matchingDomainsSet, HashSet<string> matchingFuzzyDomainsSet,
|
HashSet<string> matchingDomainsSet, HashSet<string> matchingFuzzyDomainsSet,
|
||||||
|
@ -627,7 +850,7 @@ namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
switch(cipher.Type)
|
switch(cipher.Type)
|
||||||
{
|
{
|
||||||
case Enums.CipherType.Login:
|
case CipherType.Login:
|
||||||
cipher.Login = new Login
|
cipher.Login = new Login
|
||||||
{
|
{
|
||||||
PasswordRevisionDate = model.Login.PasswordRevisionDate
|
PasswordRevisionDate = model.Login.PasswordRevisionDate
|
||||||
|
@ -652,13 +875,13 @@ namespace Bit.Core.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Enums.CipherType.SecureNote:
|
case CipherType.SecureNote:
|
||||||
cipher.SecureNote = new SecureNote
|
cipher.SecureNote = new SecureNote
|
||||||
{
|
{
|
||||||
Type = model.SecureNote.Type
|
Type = model.SecureNote.Type
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case Enums.CipherType.Card:
|
case CipherType.Card:
|
||||||
cipher.Card = new Card();
|
cipher.Card = new Card();
|
||||||
await EncryptObjPropertyAsync(model.Card, cipher.Card, new HashSet<string>
|
await EncryptObjPropertyAsync(model.Card, cipher.Card, new HashSet<string>
|
||||||
{
|
{
|
||||||
|
@ -670,7 +893,7 @@ namespace Bit.Core.Services
|
||||||
"Code"
|
"Code"
|
||||||
}, key);
|
}, key);
|
||||||
break;
|
break;
|
||||||
case Enums.CipherType.Identity:
|
case CipherType.Identity:
|
||||||
cipher.Identity = new Identity();
|
cipher.Identity = new Identity();
|
||||||
await EncryptObjPropertyAsync(model.Identity, cipher.Identity, new HashSet<string>
|
await EncryptObjPropertyAsync(model.Identity, cipher.Identity, new HashSet<string>
|
||||||
{
|
{
|
||||||
|
@ -759,5 +982,99 @@ namespace Bit.Core.Services
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
return encPhs;
|
return encPhs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CipherLocaleComparer : IComparer<CipherView>
|
||||||
|
{
|
||||||
|
private readonly II18nService _i18nService;
|
||||||
|
|
||||||
|
public CipherLocaleComparer(II18nService i18nService)
|
||||||
|
{
|
||||||
|
_i18nService = i18nService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Compare(CipherView a, CipherView b)
|
||||||
|
{
|
||||||
|
var aName = a.Name;
|
||||||
|
var bName = b.Name;
|
||||||
|
if(aName == null && bName != null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if(aName != null && bName == null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if(aName == null && bName == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var result = _i18nService.StringComparer.Compare(aName, bName);
|
||||||
|
if(result != 0 || a.Type != CipherType.Login || b.Type != CipherType.Login)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if(a.Login.Username != null)
|
||||||
|
{
|
||||||
|
aName += a.Login.Username;
|
||||||
|
}
|
||||||
|
if(b.Login.Username != null)
|
||||||
|
{
|
||||||
|
bName += b.Login.Username;
|
||||||
|
}
|
||||||
|
return _i18nService.StringComparer.Compare(aName, bName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CipherLastUsedComparer : IComparer<CipherView>
|
||||||
|
{
|
||||||
|
public int Compare(CipherView a, CipherView b)
|
||||||
|
{
|
||||||
|
var aLastUsed = a.LocalData != null && a.LocalData.ContainsKey("lastUsedDate") ?
|
||||||
|
a.LocalData["lastUsedDate"] as DateTime? : null;
|
||||||
|
var bLastUsed = b.LocalData != null && b.LocalData.ContainsKey("lastUsedDate") ?
|
||||||
|
b.LocalData["lastUsedDate"] as DateTime? : null;
|
||||||
|
|
||||||
|
var bothNotNull = aLastUsed != null && bLastUsed != null;
|
||||||
|
if(bothNotNull && aLastUsed.Value < bLastUsed.Value)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if(aLastUsed != null && bLastUsed == null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if(bothNotNull && aLastUsed.Value > bLastUsed.Value)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if(bLastUsed != null && aLastUsed == null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CipherLastUsedThenNameComparer : IComparer<CipherView>
|
||||||
|
{
|
||||||
|
private CipherLastUsedComparer _cipherLastUsedComparer;
|
||||||
|
private CipherLocaleComparer _cipherLocaleComparer;
|
||||||
|
|
||||||
|
public CipherLastUsedThenNameComparer(II18nService i18nService)
|
||||||
|
{
|
||||||
|
_cipherLastUsedComparer = new CipherLastUsedComparer();
|
||||||
|
_cipherLocaleComparer = new CipherLocaleComparer(i18nService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Compare(CipherView a, CipherView b)
|
||||||
|
{
|
||||||
|
var result = _cipherLastUsedComparer.Compare(a, b);
|
||||||
|
if(result != 0)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return _cipherLocaleComparer.Compare(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue