mirror of
https://github.com/bitwarden/mobile
synced 2025-12-16 00:03:22 +00:00
Port send jslib to mobile (#1219)
* Expand Hkdf crypto functions * Add tests for hkdf crypto functions Took the testing infrastructure from bitwarden/server * Move Hkdf to cryptoFunctionService * Port changes from bitwarden/jslib#192 * Port changes from bitwarden/jslib#205 * Make Send Expiration Optional implement changes from bitwarden/jslib#242 * Bug fixes found by testing * Test helpers * Test conversion between model types * Test SendService These are mostly happy-path tests to ensure a reasonably correct implementation * Add run tests step to GitHub Actions * Test send decryption * Test Request generation from Send * Constructor dependencies on separate lines * Remove unused testing infrastructure * Rename to match class name * Move fat arrows to previous lines * Handle exceptions in App layer * PR review cleanups * Throw when attempting to save an unkown Send Type I think it's best to only throw on unknown send types here. I don't think we want to throw whenever we encounter one since that would do bad things like lock up Sync if clients get out of date relative to servers. Instead, keep the client from ruining saved data by complaining last minute that it doesn't know what it's doing.
This commit is contained in:
@@ -206,7 +206,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\App\App.csproj">
|
||||
<Project>{9F1742A7-7D03-4BB3-8FCD-41BC3002B00A}</Project>
|
||||
<Project>{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}</Project>
|
||||
<Name>App</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Core\Core.csproj">
|
||||
|
||||
@@ -53,5 +53,12 @@ namespace Bit.Core.Abstractions
|
||||
Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request);
|
||||
Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request);
|
||||
Task PostEventsCollectAsync(IEnumerable<EventRequest> request);
|
||||
|
||||
Task<SendResponse> GetSendAsync(string id);
|
||||
Task<SendResponse> PostSendAsync(SendRequest request);
|
||||
Task<SendResponse> PostSendFileAsync(MultipartFormDataContent data);
|
||||
Task<SendResponse> PutSendAsync(string id, SendRequest request);
|
||||
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
||||
Task DeleteSendAsync(string id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ namespace Bit.Core.Abstractions
|
||||
Task<byte[]> Pbkdf2Async(byte[] password, string salt, CryptoHashAlgorithm algorithm, int iterations);
|
||||
Task<byte[]> Pbkdf2Async(string password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations);
|
||||
Task<byte[]> Pbkdf2Async(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations);
|
||||
Task<byte[]> HkdfAsync(byte[] ikm, string salt, string info, int outputByteSize, HkdfAlgorithm algorithm);
|
||||
Task<byte[]> HkdfAsync(byte[] ikm, byte[] salt, string info, int outputByteSize, HkdfAlgorithm algorithm);
|
||||
Task<byte[]> HkdfAsync(byte[] ikm, string salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm);
|
||||
Task<byte[]> HkdfAsync(byte[] ikm, byte[] salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm);
|
||||
Task<byte[]> HkdfExpandAsync(byte[] prk, string info, int outputByteSize, HkdfAlgorithm algorithm);
|
||||
Task<byte[]> HkdfExpandAsync(byte[] prk, byte[] info, int outputByteSize, HkdfAlgorithm algorithm);
|
||||
Task<byte[]> HashAsync(string value, CryptoHashAlgorithm algorithm);
|
||||
Task<byte[]> HashAsync(byte[] value, CryptoHashAlgorithm algorithm);
|
||||
Task<byte[]> HmacAsync(byte[] value, byte[] key, CryptoHashAlgorithm algorithm);
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace Bit.Core.Abstractions
|
||||
Task<Tuple<string, CipherString>> MakeKeyPairAsync(SymmetricCryptoKey key = null);
|
||||
Task<SymmetricCryptoKey> MakePinKeyAysnc(string pin, string salt, KdfType kdf, int kdfIterations);
|
||||
Task<Tuple<CipherString, SymmetricCryptoKey>> MakeShareKeyAsync();
|
||||
Task<SymmetricCryptoKey> MakeSendKeyAsync(byte[] keyMaterial);
|
||||
Task<int> RandomNumberAsync(int min, int max);
|
||||
Task<Tuple<SymmetricCryptoKey, CipherString>> RemakeEncKeyAsync(SymmetricCryptoKey key);
|
||||
Task<CipherString> RsaEncryptAsync(byte[] data, byte[] publicKey = null);
|
||||
|
||||
25
src/Core/Abstractions/ISendService.cs
Normal file
25
src/Core/Abstractions/ISendService.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.View;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface ISendService
|
||||
{
|
||||
void ClearCache();
|
||||
Task<(Send send, CipherString encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, string password,
|
||||
SymmetricCryptoKey key = null);
|
||||
Task<Send> GetAsync(string id);
|
||||
Task<List<Send>> GetAllAsync();
|
||||
Task<List<SendView>> GetAllDecryptedAsync();
|
||||
Task SaveWithServerAsync(Send sendData, byte[] encryptedFileData);
|
||||
Task UpsertAsync(params SendData[] send);
|
||||
Task ReplaceAsync(Dictionary<string, SendData> sends);
|
||||
Task ClearAsync(string userId);
|
||||
Task DeleteAsync(params string[] ids);
|
||||
Task DeleteWithServerAsync(string id);
|
||||
Task RemovePasswordWithServerAsync(string id);
|
||||
}
|
||||
}
|
||||
8
src/Core/Enums/HdkfAlgorithm.cs
Normal file
8
src/Core/Enums/HdkfAlgorithm.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum HkdfAlgorithm : byte
|
||||
{
|
||||
Sha256 = 1,
|
||||
Sha512 = 2,
|
||||
}
|
||||
}
|
||||
8
src/Core/Enums/SendType.cs
Normal file
8
src/Core/Enums/SendType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum SendType
|
||||
{
|
||||
Text = 0,
|
||||
File = 1,
|
||||
}
|
||||
}
|
||||
12
src/Core/Models/Api/SendFileApi.cs
Normal file
12
src/Core/Models/Api/SendFileApi.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
public class SendFileApi
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string SizeName { get; set; }
|
||||
}
|
||||
}
|
||||
8
src/Core/Models/Api/SendTextApi.cs
Normal file
8
src/Core/Models/Api/SendTextApi.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
public class SendTextApi
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public bool Hidden { get; set; }
|
||||
}
|
||||
}
|
||||
58
src/Core/Models/Data/SendData.cs
Normal file
58
src/Core/Models/Data/SendData.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Response;
|
||||
|
||||
namespace Bit.Core.Models.Data
|
||||
{
|
||||
public class SendData : Data
|
||||
{
|
||||
public SendData() { }
|
||||
|
||||
public SendData(SendResponse response, string userId)
|
||||
{
|
||||
Id = response.Id;
|
||||
AccessId = response.AccessId;
|
||||
UserId = userId;
|
||||
Type = response.Type;
|
||||
Name = response.Name;
|
||||
Notes = response.Notes;
|
||||
Key = response.Key;
|
||||
MaxAccessCount = response.MaxAccessCount;
|
||||
AccessCount = response.AccessCount;
|
||||
RevisionDate = response.RevisionDate;
|
||||
ExpirationDate = response.ExpirationDate;
|
||||
DeletionDate = response.DeletionDate;
|
||||
Password = response.Password;
|
||||
Disabled = response.Disabled;
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case SendType.File:
|
||||
File = new SendFileData(response.File);
|
||||
break;
|
||||
case SendType.Text:
|
||||
Text = new SendTextData(response.Text);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public string AccessId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public SendType Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public SendFileData File { get; set; }
|
||||
public SendTextData Text { get; set; }
|
||||
public string Key { get; set; }
|
||||
public int? MaxAccessCount { get; set; }
|
||||
public int AccessCount { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
public DateTime DeletionDate { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
}
|
||||
27
src/Core/Models/Data/SendFileData.cs
Normal file
27
src/Core/Models/Data/SendFileData.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Drawing;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.Models.Data
|
||||
{
|
||||
public class SendFileData : Data
|
||||
{
|
||||
public SendFileData() { }
|
||||
|
||||
public SendFileData(SendFileApi data)
|
||||
{
|
||||
Id = data.Id;
|
||||
Url = data.Url;
|
||||
FileName = data.FileName;
|
||||
Key = data.Key;
|
||||
Size = data.Size;
|
||||
SizeName = data.SizeName;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string SizeName { get; set; }
|
||||
}
|
||||
}
|
||||
19
src/Core/Models/Data/SendTextData.cs
Normal file
19
src/Core/Models/Data/SendTextData.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Drawing;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.Models.Data
|
||||
{
|
||||
public class SendTextData : Data
|
||||
{
|
||||
public SendTextData() { }
|
||||
|
||||
public SendTextData(SendTextApi data)
|
||||
{
|
||||
Text = data.Text;
|
||||
Hidden = data.Hidden;
|
||||
}
|
||||
|
||||
public string Text { get; set; }
|
||||
public bool Hidden { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ namespace Bit.Core.Models.Domain
|
||||
public string Data { get; private set; }
|
||||
public string Mac { get; private set; }
|
||||
|
||||
public async Task<string> DecryptAsync(string orgId = null)
|
||||
public async Task<string> DecryptAsync(string orgId = null, SymmetricCryptoKey key = null)
|
||||
{
|
||||
if (_decryptedValue != null)
|
||||
{
|
||||
@@ -109,8 +109,11 @@ namespace Bit.Core.Models.Domain
|
||||
var cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
try
|
||||
{
|
||||
var orgKey = await cryptoService.GetOrgKeyAsync(orgId);
|
||||
_decryptedValue = await cryptoService.DecryptToUtf8Async(this, orgKey);
|
||||
if (key == null)
|
||||
{
|
||||
key = await cryptoService.GetOrgKeyAsync(orgId);
|
||||
}
|
||||
_decryptedValue = await cryptoService.DecryptToUtf8Async(this, key);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Bit.Core.Models.Domain
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task<V> DecryptObjAsync<V, D>(V viewModel, D domain, HashSet<string> map, string orgId)
|
||||
protected async Task<V> DecryptObjAsync<V, D>(V viewModel, D domain, HashSet<string> map, string orgId, SymmetricCryptoKey key = null)
|
||||
where V : View.View
|
||||
{
|
||||
var viewModelType = viewModel.GetType();
|
||||
@@ -64,7 +64,7 @@ namespace Bit.Core.Models.Domain
|
||||
string val = null;
|
||||
if (domainPropInfo.GetValue(domain) is CipherString domainProp)
|
||||
{
|
||||
val = await domainProp.DecryptAsync(orgId);
|
||||
val = await domainProp.DecryptAsync(orgId, key);
|
||||
}
|
||||
var viewModelPropInfo = viewModelType.GetProperty(propName);
|
||||
viewModelPropInfo.SetValue(viewModel, val, null);
|
||||
|
||||
91
src/Core/Models/Domain/Send.cs
Normal file
91
src/Core/Models/Domain/Send.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public class Send : Domain
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string AccessId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public SendType Type { get; set; }
|
||||
public CipherString Name { get; set; }
|
||||
public CipherString Notes { get; set; }
|
||||
public SendFile File { get; set; }
|
||||
public SendText Text { get; set; }
|
||||
public CipherString Key { get; set; }
|
||||
public int? MaxAccessCount { get; set; }
|
||||
public int AccessCount { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
public DateTime DeletionDate { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
public Send() : base() { }
|
||||
|
||||
public Send(SendData data, bool alreadyEncrypted = false) : base()
|
||||
{
|
||||
BuildDomainModel(this, data, new HashSet<string>{
|
||||
"Id",
|
||||
"AccessId",
|
||||
"UserId",
|
||||
"Name",
|
||||
"Notes",
|
||||
"Key",
|
||||
}, alreadyEncrypted, new HashSet<string> { "Id", "AccessId", "UserId" });
|
||||
|
||||
Type = data.Type;
|
||||
MaxAccessCount = data.MaxAccessCount;
|
||||
AccessCount = data.AccessCount;
|
||||
Password = data.Password;
|
||||
Disabled = data.Disabled;
|
||||
RevisionDate = data.RevisionDate;
|
||||
DeletionDate = data.DeletionDate;
|
||||
ExpirationDate = data.ExpirationDate;
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case SendType.Text:
|
||||
Text = new SendText(data.Text, alreadyEncrypted);
|
||||
break;
|
||||
case SendType.File:
|
||||
File = new SendFile(data.File, alreadyEncrypted);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SendView> DecryptAsync()
|
||||
{
|
||||
var view = new SendView(this);
|
||||
|
||||
var cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
|
||||
view.Key = await cryptoService.DecryptToBytesAsync(Key, null);
|
||||
view.CryptoKey = await cryptoService.MakeSendKeyAsync(view.Key);
|
||||
|
||||
await DecryptObjAsync(view, this, new HashSet<string> { "Name", "Notes" }, null, view.CryptoKey);
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case SendType.File:
|
||||
view.File = await this.File.DecryptAsync(view.CryptoKey);
|
||||
break;
|
||||
case SendType.Text:
|
||||
view.Text = await this.Text.DecryptAsync(view.CryptoKey);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Core/Models/Domain/SendFile.cs
Normal file
27
src/Core/Models/Domain/SendFile.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.View;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public class SendFile : Domain
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string SizeName { get; set; }
|
||||
public CipherString FileName { get; set; }
|
||||
|
||||
public SendFile() : base() { }
|
||||
|
||||
public SendFile(SendFileData file, bool alreadyEncrypted = false) : base()
|
||||
{
|
||||
Size = file.Size;
|
||||
BuildDomainModel(this, file, new HashSet<string> { "Id", "Url", "SizeName", "FileName" }, alreadyEncrypted, new HashSet<string> { "Id", "Url", "SizeName" });
|
||||
}
|
||||
|
||||
public Task<SendFileView> DecryptAsync(SymmetricCryptoKey key) =>
|
||||
DecryptObjAsync(new SendFileView(this), this, new HashSet<string> { "FileName" }, null, key);
|
||||
}
|
||||
}
|
||||
25
src/Core/Models/Domain/SendText.cs
Normal file
25
src/Core/Models/Domain/SendText.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.View;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public class SendText : Domain
|
||||
{
|
||||
public CipherString Text { get; set; }
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
public SendText() : base() { }
|
||||
|
||||
public SendText(SendTextData data, bool alreadyEncrypted = false) : base()
|
||||
{
|
||||
Hidden = data.Hidden;
|
||||
BuildDomainModel(this, data, new HashSet<string> { "Text" }, alreadyEncrypted);
|
||||
}
|
||||
|
||||
public Task<SendTextView> DecryptAsync(SymmetricCryptoKey key) =>
|
||||
DecryptObjAsync(new SendTextView(this), this, new HashSet<string> { "Text" }, null, key);
|
||||
}
|
||||
}
|
||||
54
src/Core/Models/Request/SendRequest.cs
Normal file
54
src/Core/Models/Request/SendRequest.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.Request
|
||||
{
|
||||
public class SendRequest
|
||||
{
|
||||
public SendType Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Key { get; set; }
|
||||
public int? MaxAccessCount { get; set; }
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
public DateTime DeletionDate { get; set; }
|
||||
public SendTextApi Text { get; set; }
|
||||
public SendFileApi File { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
public SendRequest(Send send)
|
||||
{
|
||||
Type = send.Type;
|
||||
Name = send.Name?.EncryptedString;
|
||||
Notes = send.Notes?.EncryptedString;
|
||||
MaxAccessCount = send.MaxAccessCount;
|
||||
ExpirationDate = send.ExpirationDate;
|
||||
DeletionDate = send.DeletionDate;
|
||||
Key = send.Key?.EncryptedString;
|
||||
Password = send.Password;
|
||||
Disabled = send.Disabled;
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case SendType.Text:
|
||||
Text = new SendTextApi
|
||||
{
|
||||
Text = send.Text?.Text?.EncryptedString,
|
||||
Hidden = send.Text.Hidden
|
||||
};
|
||||
break;
|
||||
case SendType.File:
|
||||
File = new SendFileApi
|
||||
{
|
||||
FileName = send.File?.FileName?.EncryptedString
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Core/Models/Response/SendResponse.cs
Normal file
25
src/Core/Models/Response/SendResponse.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.Models.Response
|
||||
{
|
||||
public class SendResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string AccessId { get; set; }
|
||||
public SendType Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public SendFileApi File { get; set; }
|
||||
public SendTextApi Text { get; set; }
|
||||
public string Key { get; set; }
|
||||
public int? MaxAccessCount { get; set; }
|
||||
public int AccessCount { get; internal set; }
|
||||
public DateTime RevisionDate { get; internal set; }
|
||||
public DateTime? ExpirationDate { get; internal set; }
|
||||
public DateTime DeletionDate { get; internal set; }
|
||||
public string Password { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,6 @@ namespace Bit.Core.Models.Response
|
||||
public List<CipherResponse> Ciphers { get; set; } = new List<CipherResponse>();
|
||||
public DomainsResponse Domains { get; set; }
|
||||
public List<PolicyResponse> Policies { get; set; } = new List<PolicyResponse>();
|
||||
public List<SendResponse> Sends { get; set; } = new List<SendResponse>();
|
||||
}
|
||||
}
|
||||
|
||||
25
src/Core/Models/View/SendFileView.cs
Normal file
25
src/Core/Models/View/SendFileView.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Dynamic;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
{
|
||||
public class SendFileView : View
|
||||
{
|
||||
public SendFileView() : base() { }
|
||||
|
||||
public SendFileView(SendFile file)
|
||||
{
|
||||
Id = file.Id;
|
||||
Url = file.Url;
|
||||
Size = file.Size;
|
||||
SizeName = file.SizeName;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string SizeName { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public int FileSize => int.TryParse(Size ?? "0", out var sizeInt) ? sizeInt : 0;
|
||||
}
|
||||
}
|
||||
17
src/Core/Models/View/SendTextView.cs
Normal file
17
src/Core/Models/View/SendTextView.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
{
|
||||
public class SendTextView : View
|
||||
{
|
||||
public SendTextView() : base() { }
|
||||
public SendTextView(SendText text)
|
||||
{
|
||||
Hidden = text.Hidden;
|
||||
}
|
||||
|
||||
public string Text { get; set; } = null;
|
||||
public bool Hidden { get; set; }
|
||||
public string MaskedText => Text != null ? "••••••••" : null;
|
||||
}
|
||||
}
|
||||
45
src/Core/Models/View/SendView.cs
Normal file
45
src/Core/Models/View/SendView.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
{
|
||||
public class SendView : View
|
||||
{
|
||||
public SendView(Send send) : base()
|
||||
{
|
||||
Id = send.Id;
|
||||
AccessId = send.AccessId;
|
||||
Type = send.Type;
|
||||
MaxAccessCount = send.MaxAccessCount;
|
||||
AccessCount = send.AccessCount;
|
||||
RevisionDate = send.RevisionDate;
|
||||
DeletionDate = send.DeletionDate;
|
||||
ExpirationDate = send.ExpirationDate;
|
||||
Disabled = send.Disabled;
|
||||
Password = send.Password;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public string AccessId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public byte[] Key { get; set; }
|
||||
public SymmetricCryptoKey CryptoKey { get; set; }
|
||||
public SendType Type { get; set; }
|
||||
public SendTextView Text { get; set; } = new SendTextView();
|
||||
public SendFileView File { get; set; } = new SendFileView();
|
||||
public int? MaxAccessCount { get; set; }
|
||||
public int AccessCount { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
public DateTime DeletionDate { get; set; }
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
public string Password { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public string UrlB64Key => Key == null ? null : CoreHelpers.Base64UrlEncode(Key);
|
||||
public bool MaxAccessCountReached => MaxAccessCount.HasValue && AccessCount >= MaxAccessCount.Value;
|
||||
public bool Expired => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow;
|
||||
public bool PendingDelete => DeletionDate <= DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -215,6 +215,28 @@ namespace Bit.Core.Services
|
||||
|
||||
#endregion
|
||||
|
||||
#region Send APIs
|
||||
|
||||
public Task<SendResponse> GetSendAsync(string id) =>
|
||||
SendAsync<object, SendResponse>(HttpMethod.Get, $"/sends/{id}", null, true, true);
|
||||
|
||||
public Task<SendResponse> PostSendAsync(SendRequest request) =>
|
||||
SendAsync<SendRequest, SendResponse>(HttpMethod.Post, "/sends", request, true, true);
|
||||
|
||||
public Task<SendResponse> PostSendFileAsync(MultipartFormDataContent data) =>
|
||||
SendAsync<MultipartFormDataContent, SendResponse>(HttpMethod.Post, "/sends/file", data, true, true);
|
||||
|
||||
public Task<SendResponse> PutSendAsync(string id, SendRequest request) =>
|
||||
SendAsync<SendRequest, SendResponse>(HttpMethod.Put, $"/sends/{id}", request, true, true);
|
||||
|
||||
public Task<SendResponse> PutSendRemovePasswordAsync(string id) =>
|
||||
SendAsync<object, SendResponse>(HttpMethod.Put, $"/sends/{id}", null, true, true);
|
||||
|
||||
public Task DeleteSendAsync(string id) =>
|
||||
SendAsync<object, object>(HttpMethod.Delete, $"/sends/{id}", null, true, false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cipher APIs
|
||||
|
||||
public Task<CipherResponse> GetCipherAsync(string id)
|
||||
@@ -346,7 +368,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await HandleErrorAsync(response, false);
|
||||
var error = await HandleErrorAsync(response, false, false);
|
||||
throw new ApiException(error);
|
||||
}
|
||||
}
|
||||
@@ -398,7 +420,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await HandleErrorAsync(response, false);
|
||||
var error = await HandleErrorAsync(response, false, true);
|
||||
throw new ApiException(error);
|
||||
}
|
||||
return null;
|
||||
@@ -458,7 +480,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
else if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await HandleErrorAsync(response, false);
|
||||
var error = await HandleErrorAsync(response, false, authed);
|
||||
throw new ApiException(error);
|
||||
}
|
||||
return (TResponse)(object)null;
|
||||
@@ -506,7 +528,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await HandleErrorAsync(response, true);
|
||||
var error = await HandleErrorAsync(response, true, true);
|
||||
throw new ApiException(error);
|
||||
}
|
||||
}
|
||||
@@ -520,10 +542,10 @@ namespace Bit.Core.Services
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ErrorResponse> HandleErrorAsync(HttpResponseMessage response, bool tokenError)
|
||||
private async Task<ErrorResponse> HandleErrorAsync(HttpResponseMessage response, bool tokenError, bool authed)
|
||||
{
|
||||
if ((tokenError && response.StatusCode == HttpStatusCode.BadRequest) ||
|
||||
response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
|
||||
if (authed && ((tokenError && response.StatusCode == HttpStatusCode.BadRequest) ||
|
||||
response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden))
|
||||
{
|
||||
await _logoutCallbackAsync(true);
|
||||
return null;
|
||||
|
||||
@@ -220,7 +220,7 @@ namespace Bit.Core.Services
|
||||
throw new Exception("No public key available.");
|
||||
}
|
||||
var keyFingerprint = await _cryptoFunctionService.HashAsync(publicKey, CryptoHashAlgorithm.Sha256);
|
||||
var userFingerprint = await HkdfExpandAsync(keyFingerprint, Encoding.UTF8.GetBytes(userId), 32);
|
||||
var userFingerprint = await _cryptoFunctionService.HkdfExpandAsync(keyFingerprint, Encoding.UTF8.GetBytes(userId), 32, HkdfAlgorithm.Sha256);
|
||||
return HashPhrase(userFingerprint);
|
||||
}
|
||||
|
||||
@@ -427,6 +427,12 @@ namespace Bit.Core.Services
|
||||
return await StretchKeyAsync(pinKey);
|
||||
}
|
||||
|
||||
public async Task<SymmetricCryptoKey> MakeSendKeyAsync(byte[] keyMaterial)
|
||||
{
|
||||
var sendKey = await _cryptoFunctionService.HkdfAsync(keyMaterial, "bitwarden-send", "send", 65, HkdfAlgorithm.Sha256);
|
||||
return new SymmetricCryptoKey(sendKey);
|
||||
}
|
||||
|
||||
public async Task<string> HashPasswordAsync(string password, SymmetricCryptoKey key)
|
||||
{
|
||||
if (key == null)
|
||||
@@ -772,32 +778,13 @@ namespace Bit.Core.Services
|
||||
private async Task<SymmetricCryptoKey> StretchKeyAsync(SymmetricCryptoKey key)
|
||||
{
|
||||
var newKey = new byte[64];
|
||||
var enc = await HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("enc"), 32);
|
||||
var enc = await _cryptoFunctionService.HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("enc"), 32, HkdfAlgorithm.Sha256);
|
||||
Buffer.BlockCopy(enc, 0, newKey, 0, 32);
|
||||
var mac = await HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("mac"), 32);
|
||||
var mac = await _cryptoFunctionService.HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("mac"), 32, HkdfAlgorithm.Sha256);
|
||||
Buffer.BlockCopy(mac, 0, newKey, 32, 32);
|
||||
return new SymmetricCryptoKey(newKey);
|
||||
}
|
||||
|
||||
// ref: https://tools.ietf.org/html/rfc5869
|
||||
private async Task<byte[]> HkdfExpandAsync(byte[] prk, byte[] info, int size)
|
||||
{
|
||||
var hashLen = 32; // sha256
|
||||
var okm = new byte[size];
|
||||
var previousT = new byte[0];
|
||||
var n = (int)Math.Ceiling((double)size / hashLen);
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var t = new byte[previousT.Length + info.Length + 1];
|
||||
previousT.CopyTo(t, 0);
|
||||
info.CopyTo(t, previousT.Length);
|
||||
t[t.Length - 1] = (byte)(i + 1);
|
||||
previousT = await _cryptoFunctionService.HmacAsync(t, prk, CryptoHashAlgorithm.Sha256);
|
||||
previousT.CopyTo(okm, i * hashLen);
|
||||
}
|
||||
return okm;
|
||||
}
|
||||
|
||||
private List<string> HashPhrase(byte[] hash, int minimumEntropy = 64)
|
||||
{
|
||||
var wordLength = EEFLongWordList.Instance.List.Count;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Bit.Core.Enums;
|
||||
using PCLCrypto;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static PCLCrypto.WinRTCrypto;
|
||||
@@ -43,6 +44,61 @@ namespace Bit.Core.Services
|
||||
return Task.FromResult(_cryptoPrimitiveService.Pbkdf2(password, salt, algorithm, iterations));
|
||||
}
|
||||
|
||||
public async Task<byte[]> HkdfAsync(byte[] ikm, string salt, string info, int outputByteSize, HkdfAlgorithm algorithm) =>
|
||||
await HkdfAsync(ikm, Encoding.UTF8.GetBytes(salt), Encoding.UTF8.GetBytes(info), outputByteSize, algorithm);
|
||||
|
||||
public async Task<byte[]> HkdfAsync(byte[] ikm, byte[] salt, string info, int outputByteSize, HkdfAlgorithm algorithm) =>
|
||||
await HkdfAsync(ikm, salt, Encoding.UTF8.GetBytes(info), outputByteSize, algorithm);
|
||||
|
||||
public async Task<byte[]> HkdfAsync(byte[] ikm, string salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm) =>
|
||||
await HkdfAsync(ikm, Encoding.UTF8.GetBytes(salt), info, outputByteSize, algorithm);
|
||||
|
||||
public async Task<byte[]> HkdfAsync(byte[] ikm, byte[] salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm)
|
||||
{
|
||||
var prk = await HmacAsync(ikm, salt, HkdfAlgorithmToCryptoHashAlgorithm(algorithm));
|
||||
return await HkdfExpandAsync(prk, info, outputByteSize, algorithm);
|
||||
}
|
||||
|
||||
public async Task<byte[]> HkdfExpandAsync(byte[] prk, string info, int outputByteSize, HkdfAlgorithm algorithm) =>
|
||||
await HkdfExpandAsync(prk, Encoding.UTF8.GetBytes(info), outputByteSize, algorithm);
|
||||
|
||||
// ref: https://tools.ietf.org/html/rfc5869
|
||||
public async Task<byte[]> HkdfExpandAsync(byte[] prk, byte[] info, int outputByteSize, HkdfAlgorithm algorithm)
|
||||
{
|
||||
var hashLen = algorithm == HkdfAlgorithm.Sha256 ? 32 : 64;
|
||||
|
||||
var maxOutputByteSize = 255 * hashLen;
|
||||
if (outputByteSize > maxOutputByteSize)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(outputByteSize)} is too large. Max is {maxOutputByteSize}, received {outputByteSize}");
|
||||
}
|
||||
if (prk.Length < hashLen)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(prk)} length is too small. Must be at least {hashLen} for {algorithm}");
|
||||
}
|
||||
|
||||
var cryptoHashAlgorithm = HkdfAlgorithmToCryptoHashAlgorithm(algorithm);
|
||||
var previousT = new byte[0];
|
||||
var runningOkmLength = 0;
|
||||
var n = (int)Math.Ceiling((double)outputByteSize / hashLen);
|
||||
var okm = new byte[n * hashLen];
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var t = new byte[previousT.Length + info.Length + 1];
|
||||
previousT.CopyTo(t, 0);
|
||||
info.CopyTo(t, previousT.Length);
|
||||
t[t.Length - 1] = (byte)(i + 1);
|
||||
previousT = await HmacAsync(t, prk, cryptoHashAlgorithm);
|
||||
previousT.CopyTo(okm, runningOkmLength);
|
||||
runningOkmLength = previousT.Length;
|
||||
if (runningOkmLength >= outputByteSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return okm.Take(outputByteSize).ToArray();
|
||||
}
|
||||
|
||||
public Task<byte[]> HashAsync(string value, CryptoHashAlgorithm algorithm)
|
||||
{
|
||||
return HashAsync(Encoding.UTF8.GetBytes(value), algorithm);
|
||||
@@ -217,5 +273,18 @@ namespace Bit.Core.Services
|
||||
.Replace("\n", " ") // New line => space
|
||||
.Replace(" ", " "); // No-break space (00A0) => space
|
||||
}
|
||||
|
||||
private CryptoHashAlgorithm HkdfAlgorithmToCryptoHashAlgorithm(HkdfAlgorithm hkdfAlgorithm)
|
||||
{
|
||||
switch (hkdfAlgorithm)
|
||||
{
|
||||
case HkdfAlgorithm.Sha256:
|
||||
return CryptoHashAlgorithm.Sha256;
|
||||
case HkdfAlgorithm.Sha512:
|
||||
return CryptoHashAlgorithm.Sha512;
|
||||
default:
|
||||
throw new ArgumentException($"Invalid hkdf algorithm type, {hkdfAlgorithm}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
279
src/Core/Services/SendService.cs
Normal file
279
src/Core/Services/SendService.cs
Normal file
@@ -0,0 +1,279 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Models.View;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class SendService : ISendService
|
||||
{
|
||||
private List<SendView> _decryptedSendsCache;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private Task<List<SendView>> _getAllDecryptedTask;
|
||||
|
||||
public SendService(
|
||||
ICryptoService cryptoService,
|
||||
IUserService userService,
|
||||
IApiService apiService,
|
||||
IStorageService storageService,
|
||||
II18nService i18nService,
|
||||
ICryptoFunctionService cryptoFunctionService)
|
||||
{
|
||||
_cryptoService = cryptoService;
|
||||
_userService = userService;
|
||||
_apiService = apiService;
|
||||
_storageService = storageService;
|
||||
_i18nService = i18nService;
|
||||
_cryptoFunctionService = cryptoFunctionService;
|
||||
}
|
||||
|
||||
public static string GetSendKey(string userId) => string.Format("sends_{0}", userId);
|
||||
|
||||
public async Task ClearAsync(string userId)
|
||||
{
|
||||
await _storageService.RemoveAsync(GetSendKey(userId));
|
||||
ClearCache();
|
||||
}
|
||||
|
||||
public void ClearCache() => _decryptedSendsCache = null;
|
||||
|
||||
public async Task DeleteAsync(params string[] ids)
|
||||
{
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
var sends = await _storageService.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId));
|
||||
|
||||
if (sends == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
sends.Remove(id);
|
||||
}
|
||||
|
||||
await _storageService.SaveAsync(GetSendKey(userId), sends);
|
||||
ClearCache();
|
||||
}
|
||||
|
||||
public async Task DeleteWithServerAsync(string id)
|
||||
{
|
||||
await _apiService.DeleteSendAsync(id);
|
||||
await DeleteAsync(id);
|
||||
}
|
||||
|
||||
public async Task<(Send send, CipherString encryptedFileData)> EncryptAsync(SendView model, byte[] fileData,
|
||||
string password, SymmetricCryptoKey key = null)
|
||||
{
|
||||
if (model.Key == null)
|
||||
{
|
||||
model.Key = _cryptoFunctionService.RandomBytes(16);
|
||||
model.CryptoKey = await _cryptoService.MakeSendKeyAsync(model.Key);
|
||||
}
|
||||
|
||||
var send = new Send
|
||||
{
|
||||
Id = model.Id,
|
||||
Type = model.Type,
|
||||
Disabled = model.Disabled,
|
||||
MaxAccessCount = model.MaxAccessCount,
|
||||
Key = await _cryptoService.EncryptAsync(model.Key, key),
|
||||
Name = await _cryptoService.EncryptAsync(model.Name, model.CryptoKey),
|
||||
Notes = await _cryptoService.EncryptAsync(model.Notes, model.CryptoKey),
|
||||
};
|
||||
CipherString encryptedFileData = null;
|
||||
|
||||
if (password != null)
|
||||
{
|
||||
var passwordHash = await _cryptoFunctionService.Pbkdf2Async(password, model.Key,
|
||||
CryptoHashAlgorithm.Sha256, 100000);
|
||||
send.Password = Convert.ToBase64String(passwordHash);
|
||||
}
|
||||
|
||||
switch (send.Type)
|
||||
{
|
||||
case SendType.Text:
|
||||
send.Text = new SendText
|
||||
{
|
||||
Text = await _cryptoService.EncryptAsync(model.Text.Text, model.CryptoKey),
|
||||
Hidden = model.Text.Hidden
|
||||
};
|
||||
break;
|
||||
case SendType.File:
|
||||
send.File = new SendFile();
|
||||
if (fileData != null)
|
||||
{
|
||||
send.File.FileName = await _cryptoService.EncryptAsync(model.File.FileName, model.CryptoKey);
|
||||
encryptedFileData = await _cryptoService.EncryptAsync(fileData, model.CryptoKey);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (send, encryptedFileData);
|
||||
}
|
||||
|
||||
public async Task<List<Send>> GetAllAsync()
|
||||
{
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
var sends = await _storageService.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId));
|
||||
return sends.Select(kvp => new Send(kvp.Value)).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<SendView>> GetAllDecryptedAsync()
|
||||
{
|
||||
if (_decryptedSendsCache != null)
|
||||
{
|
||||
return _decryptedSendsCache;
|
||||
}
|
||||
|
||||
var hasKey = await _cryptoService.HasKeyAsync();
|
||||
if (!hasKey)
|
||||
{
|
||||
throw new Exception("No Key.");
|
||||
}
|
||||
|
||||
if (_getAllDecryptedTask != null && !_getAllDecryptedTask.IsCompleted && !_getAllDecryptedTask.IsFaulted)
|
||||
{
|
||||
return await _getAllDecryptedTask;
|
||||
}
|
||||
|
||||
async Task<List<SendView>> doTask()
|
||||
{
|
||||
var decSends = new List<SendView>();
|
||||
|
||||
async Task decryptAndAddSendAsync(Send send) => decSends.Add(await send.DecryptAsync());
|
||||
await Task.WhenAll((await GetAllAsync()).Select(s => decryptAndAddSendAsync(s)));
|
||||
|
||||
decSends.OrderBy(s => s, new SendLocaleComparer(_i18nService)).ToList();
|
||||
_decryptedSendsCache = decSends;
|
||||
return _decryptedSendsCache;
|
||||
}
|
||||
|
||||
_getAllDecryptedTask = doTask();
|
||||
return await _getAllDecryptedTask;
|
||||
}
|
||||
|
||||
public async Task<Send> GetAsync(string id)
|
||||
{
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
var sends = await _storageService.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId));
|
||||
|
||||
if (sends == null || !sends.ContainsKey(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Send(sends[id]);
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Dictionary<string, SendData> sends)
|
||||
{
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
await _storageService.SaveAsync(GetSendKey(userId), sends);
|
||||
_decryptedSendsCache = null;
|
||||
}
|
||||
|
||||
public async Task SaveWithServerAsync(Send send, byte[] encryptedFileData)
|
||||
{
|
||||
|
||||
var request = new SendRequest(send);
|
||||
SendResponse response;
|
||||
if (send.Id == null)
|
||||
{
|
||||
switch (send.Type)
|
||||
{
|
||||
case SendType.Text:
|
||||
response = await _apiService.PostSendAsync(request);
|
||||
break;
|
||||
case SendType.File:
|
||||
var fd = new MultipartFormDataContent($"--BWMobileFormBoundary{DateTime.UtcNow.Ticks}")
|
||||
{
|
||||
{ new StringContent(JsonConvert.SerializeObject(request)), "model" },
|
||||
{ new ByteArrayContent(encryptedFileData), "data", send.File.FileName.EncryptedString }
|
||||
};
|
||||
|
||||
response = await _apiService.PostSendFileAsync(fd);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException($"Cannot save unknown Send type {send.Type}");
|
||||
}
|
||||
send.Id = response.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
response = await _apiService.PutSendAsync(send.Id, request);
|
||||
}
|
||||
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
await UpsertAsync(new SendData(response, userId));
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(params SendData[] sends)
|
||||
{
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
var knownSends = await _storageService.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)) ??
|
||||
new Dictionary<string, SendData>();
|
||||
|
||||
foreach (var send in sends)
|
||||
{
|
||||
knownSends[send.Id] = send;
|
||||
}
|
||||
|
||||
await _storageService.SaveAsync(GetSendKey(userId), knownSends);
|
||||
_decryptedSendsCache = null;
|
||||
}
|
||||
|
||||
public async Task RemovePasswordWithServerAsync(string id)
|
||||
{
|
||||
var response = await _apiService.PutSendRemovePasswordAsync(id);
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
await UpsertAsync(new SendData(response, userId));
|
||||
}
|
||||
|
||||
private class SendLocaleComparer : IComparer<SendView>
|
||||
{
|
||||
private readonly II18nService _i18nService;
|
||||
|
||||
public SendLocaleComparer(II18nService i18nService)
|
||||
{
|
||||
_i18nService = i18nService;
|
||||
}
|
||||
|
||||
public int Compare(SendView a, SendView 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;
|
||||
}
|
||||
return _i18nService.StringComparer.Compare(aName, bName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ namespace Bit.Core.Services
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ISendService _sendService;
|
||||
private readonly Func<bool, Task> _logoutCallbackAsync;
|
||||
|
||||
public SyncService(
|
||||
@@ -37,6 +38,7 @@ namespace Bit.Core.Services
|
||||
IStorageService storageService,
|
||||
IMessagingService messagingService,
|
||||
IPolicyService policyService,
|
||||
ISendService sendService,
|
||||
Func<bool, Task> logoutCallbackAsync)
|
||||
{
|
||||
_userService = userService;
|
||||
@@ -49,6 +51,7 @@ namespace Bit.Core.Services
|
||||
_storageService = storageService;
|
||||
_messagingService = messagingService;
|
||||
_policyService = policyService;
|
||||
_sendService = sendService;
|
||||
_logoutCallbackAsync = logoutCallbackAsync;
|
||||
}
|
||||
|
||||
@@ -104,7 +107,8 @@ namespace Bit.Core.Services
|
||||
await SyncCollectionsAsync(response.Collections);
|
||||
await SyncCiphersAsync(userId, response.Ciphers);
|
||||
await SyncSettingsAsync(userId, response.Domains);
|
||||
await SyncPolicies(response.Policies);
|
||||
await SyncPoliciesAsync(response.Policies);
|
||||
await SyncSendsAsync(userId, response.Sends);
|
||||
await SetLastSyncAsync(now);
|
||||
return SyncCompleted(true);
|
||||
}
|
||||
@@ -363,11 +367,14 @@ namespace Bit.Core.Services
|
||||
await _settingsService.SetEquivalentDomainsAsync(eqDomains);
|
||||
}
|
||||
|
||||
private async Task SyncPolicies(List<PolicyResponse> response)
|
||||
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 Task SyncSendsAsync(string userId, List<SendResponse> sends) =>
|
||||
_sendService.ReplaceAsync(sends.ToDictionary(s => userId, s => new SendData(s, userId)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,5 +244,10 @@ namespace Bit.Core.Utilities
|
||||
// Standard base64 decoder
|
||||
return Convert.FromBase64String(output);
|
||||
}
|
||||
|
||||
public static T Clone<T>(T obj)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,10 @@ namespace Bit.Core.Utilities
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
var policyService = new PolicyService(storageService, userService);
|
||||
var sendService = new SendService(cryptoService, userService, apiService, storageService, i18nService,
|
||||
cryptoFunctionService);
|
||||
var syncService = new SyncService(userService, apiService, settingsService, folderService,
|
||||
cipherService, cryptoService, collectionService, storageService, messagingService, policyService,
|
||||
cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService,
|
||||
(bool expired) =>
|
||||
{
|
||||
messagingService.Send("logout", expired);
|
||||
|
||||
Reference in New Issue
Block a user