bitwarden-android/src/Core/Services/CipherService.cs

404 lines
15 KiB
C#
Raw Normal View History

2019-04-12 17:06:47 +03:00
using Bit.Core.Abstractions;
2019-04-15 18:46:20 +03:00
using Bit.Core.Enums;
using Bit.Core.Models.Data;
2019-04-12 17:06:47 +03:00
using Bit.Core.Models.Domain;
using Bit.Core.Models.View;
using System;
using System.Collections.Generic;
2019-04-15 18:46:20 +03:00
using System.Linq;
2019-04-12 17:06:47 +03:00
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public class CipherService
{
private const string Keys_CiphersFormat = "ciphers_{0}";
private const string Keys_LocalData = "ciphersLocalData";
private const string Keys_NeverDomains = "neverDomains";
private List<CipherView> _decryptedCipherCache;
private readonly ICryptoService _cryptoService;
private readonly IUserService _userService;
private readonly ISettingsService _settingsService;
private readonly IApiService _apiService;
private readonly IStorageService _storageService;
private readonly II18nService _i18NService;
private Dictionary<string, HashSet<string>> _domainMatchBlacklist = new Dictionary<string, HashSet<string>>
{
["google.com"] = new HashSet<string> { "script.google.com" }
};
public CipherService(
ICryptoService cryptoService,
IUserService userService,
ISettingsService settingsService,
IApiService apiService,
IStorageService storageService,
II18nService i18nService)
{
_cryptoService = cryptoService;
_userService = userService;
_settingsService = settingsService;
_apiService = apiService;
_storageService = storageService;
_i18NService = i18nService;
}
private List<CipherView> DecryptedCipherCache
{
get => _decryptedCipherCache;
set
{
if(value == null)
{
_decryptedCipherCache.Clear();
}
_decryptedCipherCache = value;
// TODO: update search index
}
}
public void ClearCache()
{
DecryptedCipherCache = null;
}
public async Task<Cipher> Encrypt(CipherView model, SymmetricCryptoKey key = null,
Cipher originalCipher = null)
{
// Adjust password history
if(model.Id != null)
{
2019-04-15 18:46:20 +03:00
if(originalCipher == null)
{
originalCipher = await GetAsync(model.Id);
}
if(originalCipher != null)
{
var existingCipher = await originalCipher.DecryptAsync();
if(model.PasswordHistory == null)
{
model.PasswordHistory = new List<PasswordHistoryView>();
}
if(model.Type == CipherType.Login && existingCipher.Type == CipherType.Login)
{
if(!string.IsNullOrWhiteSpace(existingCipher.Login.Password) &&
existingCipher.Login.Password != model.Login.Password)
{
var now = DateTime.UtcNow;
var ph = new PasswordHistoryView
{
Password = existingCipher.Login.Password,
LastUsedDate = now
};
model.Login.PasswordRevisionDate = now;
model.PasswordHistory.Insert(0, ph);
}
else
{
model.Login.PasswordRevisionDate = DateTime.UtcNow;
}
}
if(existingCipher.HasFields)
{
var existingHiddenFields = existingCipher.Fields.Where(f =>
f.Type == FieldType.Hidden && !string.IsNullOrWhiteSpace(f.Name) &&
!string.IsNullOrWhiteSpace(f.Value));
var hiddenFields = model.Fields?.Where(f =>
f.Type == FieldType.Hidden && !string.IsNullOrWhiteSpace(f.Name)) ??
new List<FieldView>();
foreach(var ef in existingHiddenFields)
{
var matchedField = hiddenFields.FirstOrDefault(f => f.Name == ef.Name);
if(matchedField == null || matchedField.Value != ef.Value)
{
var ph = new PasswordHistoryView
{
Password = string.Format("{0}: {1}", ef.Name, ef.Value),
LastUsedDate = DateTime.UtcNow
};
model.PasswordHistory.Insert(0, ph);
}
}
}
}
if(!model.PasswordHistory?.Any() ?? false)
{
model.PasswordHistory = null;
}
else if(model.PasswordHistory != null && model.PasswordHistory.Count > 5)
{
model.PasswordHistory = model.PasswordHistory.Take(5).ToList();
}
2019-04-12 17:06:47 +03:00
}
2019-04-15 18:46:20 +03:00
var cipher = new Cipher
{
Id = model.Id,
FolderId = model.FolderId,
Favorite = model.Favorite,
OrganizationId = model.OrganizationId,
Type = model.Type,
CollectionIds = model.CollectionIds
};
2019-04-12 17:06:47 +03:00
if(key == null && cipher.OrganizationId != null)
{
key = await _cryptoService.GetOrgKeyAsync(cipher.OrganizationId);
if(key == null)
{
throw new Exception("Cannot encrypt cipher for organization. No key.");
}
}
2019-04-15 18:46:20 +03:00
var tasks = new List<Task>
2019-04-12 17:06:47 +03:00
{
2019-04-15 18:46:20 +03:00
EncryptObjPropertyAsync(model, cipher, new HashSet<string>
{
"Name",
"Notes"
}, key),
EncryptCipherDataAsync(cipher, model, key),
EncryptFieldsAsync(model.Fields, key)
.ContinueWith(async fields => cipher.Fields = await fields),
EncryptPasswordHistoriesAsync(model.PasswordHistory, key)
.ContinueWith(async phs => cipher.PasswordHistory = await phs),
EncryptAttachmentsAsync(model.Attachments, key)
.ContinueWith(async attachments => cipher.Attachments = await attachments)
};
2019-04-12 17:06:47 +03:00
await Task.WhenAll(tasks);
return cipher;
}
2019-04-15 18:46:20 +03:00
public async Task<Cipher> GetAsync(string id)
{
var userId = await _userService.GetUserIdAsync();
var localData = await _storageService.GetAsync<Dictionary<string, Dictionary<string, object>>>(
Keys_LocalData);
var ciphers = await _storageService.GetAsync<Dictionary<string, CipherData>>(
string.Format(Keys_CiphersFormat, userId));
if(!ciphers?.ContainsKey(id) ?? true)
{
return null;
}
return new Cipher(ciphers[id], false,
localData?.ContainsKey(id) ?? false ? localData[id] : null);
}
public async Task<List<Cipher>> GetAllAsync()
{
var userId = await _userService.GetUserIdAsync();
var localData = await _storageService.GetAsync<Dictionary<string, Dictionary<string, object>>>(
Keys_LocalData);
var ciphers = await _storageService.GetAsync<Dictionary<string, CipherData>>(
string.Format(Keys_CiphersFormat, userId));
var response = ciphers.Select(c => new Cipher(c.Value, false,
localData?.ContainsKey(c.Key) ?? false ? localData[c.Key] : null));
return response.ToList();
}
2019-04-12 17:06:47 +03:00
private Task EncryptObjPropertyAsync<V, D>(V model, D obj, HashSet<string> map, SymmetricCryptoKey key)
where V : View
where D : Domain
{
var modelType = model.GetType();
var objType = obj.GetType();
Task<CipherString> makeCs(string propName)
{
var modelPropInfo = modelType.GetProperty(propName);
var modelProp = modelPropInfo.GetValue(model) as string;
if(!string.IsNullOrWhiteSpace(modelProp))
{
return _cryptoService.EncryptAsync(modelProp, key);
}
return Task.FromResult((CipherString)null);
};
void setCs(string propName, CipherString val)
{
var objPropInfo = objType.GetProperty(propName);
objPropInfo.SetValue(obj, val, null);
};
var tasks = new List<Task>();
foreach(var prop in map)
{
tasks.Add(makeCs(prop).ContinueWith(async val => setCs(prop, await val)));
}
return Task.WhenAll(tasks);
}
2019-04-15 18:46:20 +03:00
private async Task<List<Attachment>> EncryptAttachmentsAsync(
List<AttachmentView> attachmentsModel, SymmetricCryptoKey key)
{
if(!attachmentsModel?.Any() ?? true)
{
return null;
}
var tasks = new List<Task>();
var encAttachments = new List<Attachment>();
foreach(var model in attachmentsModel)
{
var attachment = new Attachment
{
Id = model.Id,
Size = model.Size,
SizeName = model.SizeName,
Url = model.Url
};
var task = EncryptObjPropertyAsync(model, attachment, new HashSet<string>
{
"FileName"
}, key).ContinueWith(async (t) =>
{
if(model.Key != null)
{
attachment.Key = await _cryptoService.EncryptAsync(model.Key.Key, key);
}
encAttachments.Add(attachment);
});
tasks.Add(task);
}
await Task.WhenAll(tasks);
return encAttachments;
}
private async Task EncryptCipherDataAsync(Cipher cipher, CipherView model, SymmetricCryptoKey key)
{
switch(cipher.Type)
{
case Enums.CipherType.Login:
cipher.Login = new Login
{
PasswordRevisionDate = model.Login.PasswordRevisionDate
};
await EncryptObjPropertyAsync(model.Login, cipher.Login, new HashSet<string>
{
"Username",
"Password",
"Totp"
}, key);
if(model.Login.Uris != null)
{
cipher.Login.Uris = new List<LoginUri>();
foreach(var uri in model.Login.Uris)
{
var loginUri = new LoginUri
{
Match = uri.Match
};
await EncryptObjPropertyAsync(uri, loginUri, new HashSet<string> { "Uri" }, key);
cipher.Login.Uris.Add(loginUri);
}
}
break;
case Enums.CipherType.SecureNote:
cipher.SecureNote = new SecureNote
{
Type = model.SecureNote.Type
};
break;
case Enums.CipherType.Card:
cipher.Card = new Card();
await EncryptObjPropertyAsync(model.Card, cipher.Card, new HashSet<string>
{
"CardholderName",
"Brand",
"Number",
"ExpMonth",
"ExpYear",
"Code"
}, key);
break;
case Enums.CipherType.Identity:
cipher.Identity = new Identity();
await EncryptObjPropertyAsync(model.Identity, cipher.Identity, new HashSet<string>
{
"Title",
"FirstName",
"MiddleName",
"LastName",
"Address1",
"Address2",
"Address3",
"City",
"State",
"PostalCode",
"Country",
"Company",
"Email",
"Phone",
"SSN",
"Username",
"PassportNumber",
"LicenseNumber"
}, key);
break;
default:
throw new Exception("Unknown cipher type.");
}
}
private async Task<List<Field>> EncryptFieldsAsync(List<FieldView> fieldsModel, SymmetricCryptoKey key)
{
if(!fieldsModel?.Any() ?? true)
{
return null;
}
var tasks = new List<Task>();
var encFields = new List<Field>();
foreach(var model in fieldsModel)
{
var field = new Field
{
Type = model.Type
};
// normalize boolean type field values
if(model.Type == FieldType.Boolean && model.Value != "true")
{
model.Value = "false";
}
var task = EncryptObjPropertyAsync(model, field, new HashSet<string>
{
"Name",
"Value"
}, key).ContinueWith((t) =>
{
encFields.Add(field);
});
tasks.Add(task);
}
await Task.WhenAll(tasks);
return encFields;
}
private async Task<List<PasswordHistory>> EncryptPasswordHistoriesAsync(List<PasswordHistoryView> phModels,
SymmetricCryptoKey key)
{
if(!phModels?.Any() ?? true)
{
return null;
}
var tasks = new List<Task>();
var encPhs = new List<PasswordHistory>();
foreach(var model in phModels)
{
var ph = new PasswordHistory
{
LastUsedDate = model.LastUsedDate
};
var task = EncryptObjPropertyAsync(model, ph, new HashSet<string>
{
"Password"
}, key).ContinueWith((t) =>
{
encPhs.Add(ph);
});
tasks.Add(task);
}
await Task.WhenAll(tasks);
return encPhs;
}
2019-04-12 17:06:47 +03:00
}
}